–PAGE_BREAK–В качестве примера рассмотрим объектный тип TString для представления строковых данных с более высокой степенью защиты от ошибок, чем это обеспечено стандартными функциями обработки строк из файла-заголовка string.h.
Ниже приведен пример программы, иллюстрирующей использование данных типа TString.
Описание конструктора можно упростить, если компоненты-данные принадлежат к базовым типам или являются объектными переменными, имеющими конструктор. При описании конструктора после заголовка функции можно поставить двоеточие и за ним список инициализаторов вида идентификатор (аргументы ). Например, для класса TPoint из предыдущего параграфа можно было определить конструктор так:
В этом конструкторе все компоненты получают значения из списка инициализации, а тело конструктора представлено пустым составным оператором.
4.3. Производные классы Классы образуют иерархическую структуру, когда выделяется некоторый базовый класс, содержащий общие данные и методы группы сходных классов, и строится несколько производных классов, в которых к данным и методам базового класса добавляются данные и методы, необходимые для реализации производного класса. Описание системы классов в этом случае выглядит так:
Доступом к компонентам базового класса управляют ключевые слова public и private. Если базовый класс public, то в производном классе public-компоненты базового класса останутся public, protected-компоненты базового класса останутся protected, private-компоненты базового класса для функций производного класса будут недоступны.
Если базовый класс private, то в производном классе public и protected компоненты базового класса доступны для функций производного класса, но для следующего производного класса они будут считаться private, т.е. будут недоступны, private-компоненты базового класса недоступны в производных классах.
Конструктор производного класса должен вызывать конструктор своего базового класса:
4.4. Пример построения системы классов Известно, что при объявлении массивов в Си/Си++ количество элементов массива задается константой и в дальнейшем не может быть изменено. При обращении к элементам массив отсутствует контроль выхода за пределы индексов массива, что приводит к трудно обнаруживамым ошибкам в программах. Построим систему классов для обработки динамических массивов, в которые можно добавлять новые элементы и исключить возможность выхода за пределы текущего размера массива. Общие свойства массивов с такими сойствами, не зависящие от типа элементов массива, объединим в классе TBase, а для массивов с различными типами элеменов образуем свои классы. Описания классов объединим в файле заголовков TBASEARR.H, а определения методов приведем в файле TBASEARR.CPP.
Определения методов приведены в файле TBASEARR.CPP:
4.5 Виртуальные функции 4.5.1. Понятие о “позднем” связывании При описании объектных типов функции, имеющие сходное назначение в разных классах, могут иметь одинаковые имена, типы параметров и возвращаемого значения. При обращении к такой функции с указанием имени объекта компилятору известно, какая из одноименных функций требуется. В то же время к объектам производного типа можно обращаться по указателю на базовый тип и тогда на этапе компиляции нельзя установить, функция какого из производных типов должна быть вызвана. В ходе выполнения программы требуется проверять, на объект какого типа ссылается указатель и после такой проверки вызывать требуемую функцию. Эти действия называют “поздним” связыванием, в отличие от “раннего” связывания, при котором уже на этапах компиляции или редактирования связей можно установить адрес точки входа вызываемой функции. В объектно-ориентированных языках программирования для решения этой проблемы применяются виртуальные методы.
4.5.2. Описание виртуальных функций Функция-компонента класса объявляется как виртуальная указанием ключевого слова virtual. Функции-компоненты в производных классах, заменяющие виртуальную функцию базового класса должны объявляться с тем же именем, тем же списком параметров и типом возвращаемого значения, что и соответствующая функция базового класса. Если из производного класса не образуется новых производных классов, ключевое слово virtual в описании функции можно опустить.
Если в производном классе нет объявления функции с тем же именем, что и виртуальная функция базового класса, будет вызываться функция базового класса.
Виртуальная функция может быть объявлена в форме:
Такая функция называется “чистой” (pure) виртуальной функцией, а объектный тип, содержащий ее объявление, называется абстрактным объектным типом. В программе не могут создаваться экземпляры абстрактных типов, такой тип может использоваться только для образования производных типов, причем в производном типе следует либо снова определить эту виртуальную функцию как чистую, либо обявить ее как обычную виртуальную функцию, выполняющую конкретные действия.
Виртуальные функции особенно полезны, когда к методом класса требуется обращаться через указатель на экземпляр класса, а сам этот указатель имеет тип указателя на базовый класс. Пусть, например, в классе TBase объявлена чистая виртуальная функция print:
Тогда в производных классах должна быть объявлена замещающая ее функция print, выполняющая реальные действия:
В программе, использующей объекты классов TIntArray и TRealArray могут создаваться экземпляры этих классов с возможностью обращения к ним через указатель на базовый класс:
Тогда для печати массивов могут применяться операторы
Приведем еще один пример использования виртуальных функций. Пусть некоторый любитель домашних животных решил завести каталог своих любимцев и для каждого вида животных определил свой класс с общим базовым классом Pet. Для краткости ограничимся в описании каждого животного его кличкой и типовым излаваемым животным звуком с возможностью вывода на экран списка кличек и представления издаваемых ими звуков.
Программа:
4.6. “Дружественные” (friend) функции Функция, объявленная в производном классе, может иметь доступ только к защищенным (protected) или общим (public) компонентам базового класса.
Функция, объявленная вне класса, может иметь доступ только к общим (public) компонентам класса и обращаться к ним по имени, уточненному именем объекта или указателя на объект.
Чтобы получить доступ к личным компонентам объектов некоторого класса Х в функции, не имеющей к ним доступа, эта функция должна быть объявлена дружественной в классе X:
Можно объявить все функции класса Y дружественными в классе X;
Дружественной может быть и функция, не являющаяся компонентой какого-либо класса, например,
Здесь функция printXX имеет доступ ко всем компонентам класса XX, независимо от закрепленного за ними уровня доступа.
В теории объектно-ориентированного программирования считается, что при хорошо спроектированной системе классов не должно быть необходимости в дружественных функциях, однако в ряде случаев их использование упрощает понимание и последующие модификации программы.
4.7. Статические компоненты класса Описатель static в С++ имеет различное назначение в зависимости от контекста, в котором он применен.
Переменные и функции, объявленные вне класса и вне тела функции с описателем static, имеют область действия, ограниченную файлом, в котором они объявлены.
Переменные, объявленные как static внутри функции, видимы только внутри этой функции, но сохраняют свои значения после выхода из функции и инициализируются только при первом обращении к функции.
Компоненты класса также могут объявляться с описателем static, такие компоненты — данные являются общими для всех экземпляров объектов этого класса и размещаются в памяти отдельно от данных объектов класса. Доступ к static — компонентам класса возможен по имени, уточненному именем класса (именем типа) или именем объекта этого класса, причем к static — компонентам класса можно обращаться до создания экземпляров объектов этого класса. Статическое данное — член класса должно быть обязательно инициализировано вне описания класса:
Статические компоненты — функции могут вызываться до создания экземпляров объектов этого класса и поэтому имеют доступ только к статическим данным класса:
4.8. Переопределение (перегрузка) операций В языках программирования определена семантика операций, выполняемых над базовыми (предопределенными) типами данных, например, если x, y и z — переменные типа float, то запись x = y + z; предполагает интуитивно очевидные действия, сложение x и y и присваивание переменной z полученной суммы.
продолжение
–PAGE_BREAK–Желательно было бы и для типов, определяемых в программе, в том числе для классов, определить семантику и алгоритмы операций сложения, вычитания, умножения и т.д., чтобы иметь возможность вместо вызова соответствующих функций записывать просто x + y и в случае, когда x и y являются объектами некоторых классов. В C++ это достигается переопределением имеющихся в языке операций для других типов данных.
Переопределенная операция объявляется так:
Например:
Полное определение этих операций для объектов класса TPoint имеет вид:
Остальные операции определяются аналогичным образом.
Пусть в программе имеются объявления:
Тогда можно записать:
Общие правила переопределения операций сводятся к следующему:
— Двуместные операции должны иметь два параметра, одноместные — один параметр, причем, если операция объявлена как компонента класса, то неявным первым операндом является экземпляр объекта (следовательно при определении двуместной операции будет задаваться один параметр, одноместная операция объявляется с пустым списком параметров). Если операция переопределяется вне класса (с описателем friend ), то для двуместной операции должны быть заданы два параметра, для одноместной операции — один параметр.
— При переопределении сохраняется приоритет исходной операции т.е. операция + будет выполняться раньше операции = и т.д.
— При переопределении не наследуются свойства коммутативности и ассциативности, т.е. результат выражения х + y — z может отличаться от результата выражения y — z + x и зависит от того, как определены соответствующие операции.
— Не допускается переопределение операций. (точка), .* ( точка -звездочка, обращение к указателю на компоненту класса или структуры), :: (разрешение контекста), а также операции # и ##, используемые при препроцессорной обработке.
— Переопределяемые операции = (присваивание), () (функция), [ ] (индекс), -> (обращение к компоненте класса по указателю) всегда должны быть компонентами класса и не могут быть static.
— Переопределяемые операции new и delete должны быть static — компонентами класса.
В остальном к переопределяемым операциям предъявляются те же требования, что и к функциям.
5. Шаблоны функций и классов 5.1. Шаблоны функций Часто встречаются функции, реализующие одни и те же действия для аргументов различных типов. Например, сортировка массива по возрастанию его элементов может выполняться одним и тем же методом и для данных типа int и для данных типа double. Различие состоит только в типах параметров и некоторых внутренних переменных.
В более поздние версии С++ включено специальное средство, позволяющее параметризовать определение функции, чтобы компилятор мог построить конкретную реализацию функции для указанного типа параметров функции. Параметризованное определение функции строится по схеме:
Имя класса является параметром и задается идентификатором, локализованным в пределах определения функции. Хотя бы один из параметров функции должен иметь тип, соответствующий этому идентификатору.
Параметризованное определение функции сортировки массива методом перестановок может быть построено следующим образом:
Если в программе будут объявлены массивы
и установлены значения элементов этих массивов, то вызов функции обеспечит вызов sort для упорядочения массива целых, а вызов функции обеспечит вызов sort для упорядочения массива с элементами типа double. Если элементами массива являются объекты какого-либо определенного программистом класса, для которого определена операция отношения >, то функция sort может быть вызвана и для такого массива. Разумеется, в объектном коде программы будут присутствовать все варианты реально вызывамой функции sort. Параметризация функции сокращает объем исходного текста программы и повышает его надежность.
В описателе template можно указывать несколько параметров вида class имя_типа, а также параметры базовых типов. Например, функция
копирует первые n элементов массива b типа T2 в первые n элементов массива a типа T1. Разумеется, программист несет ответственность за то, чтобы такое копирование было возможным. 5.2. Шаблоны классов По аналогии с параметризованной функцией можно построить параметризованное описание класса, позволяющее создавать экземпляры классов для конкретных значений параметров. Параметризованный класс описывается следующим образом:
Как и для функций, в описателе template может быть задано несколько параметров. В самом описание класса имена параметров используются как имена типов данных, типов параметров функций и типов значений, возвращаемых функциями.
В качестве примера приведем описание класса stack, предназначенного для построения стеков фиксированного максимального размера с элементами произволного типа.
Следует отметить, что в этом примере с целью сокращения исходного текста не предусмотрен контроль выхода за пределы стека в методах push и pop.
Чтобы создать экземпляр параметризованного объектного типа, нужно уточнить имя типа значением параметра в угловых скобках:
stack stack_of_int (50); /* Стек на 50 элементов типа int */
stack stmc (20); /* Стек на 20 элементов типа myClass */
В приведенном примере все компоненты-функции определены в описании класса. Когда полное определение функции-члена класса задается вне описания класса, оно должно уточняться описателем template. Например, если бы метод top_of был определен вне описания класса, определение имело бы вид:
template
Type top_of ( ) { return s [ top ];}
Отметим некоторые специфические черты описаний параметризованных классов.
Если в параметризованном классе определены friend-функции, то когда такая функция не зависит от параметра, будет использоваться единственная friend-функция для всех значений параметра, а когда friend-функция зависит от параметра, будет использоваться своя friend-функция для каждого значения параметра.
Если в параметризованном классе имеются статические (static) компоненты, то для каждого значения параметра будет использоваться свой экземпляр статической компоненты.
6. Классы для ввода-вывода потоков 6.1. Система классов ввода-вывода Система ввода-вывода С++ основывается на концепции потоков данных, поток представляет собой, с одной стороны, последовательность данных, с другой стороны поток рассматривается как переменная некоторого объектного типа. Это позволяет вынести общие свойства и операции процессов ввода-вывода в определения базовых классов. Ввод-вывод из файлов и консоли, как правило, выполняется с использованием буфера и получение данных программой или вывод данных сводится к пересылке данных из одной области памяти в другую. Реальное обращение к внешним устройствам происходит только при исчерпании данных в буфере (при вводе) или при заполнении буфера (при выводе).
Система классов ввода-вывода С++ использует два базовых класса: класс ios и класс streambuf.
В классе ios определены данные, характеризующие состояние потока, и функции, позволяющие получить доступ к информации о состоянии потока или изменить его состояние. Состояние потока определяется набором битовых флагов, для обращения к отдельным флагам в классе ios описаны перечислимые константы:
— биты состояния (статуса) потока
— биты режима использования потока (режима ввода/вывода)
— флаги направления позиционирования в потоке
— флаги — манипуляторы управления вводом/выводом
Поскольку эти перечислимые константы объявлены как компоненты класса ios, для доступа к ним требуется уточнение контекста, например, ios::in.
Класс streambuf обеспечивает создание и использование буфера ввода-вывода и содержит компоненты-данные для управления буфером и методы доступа к данным в буфере. Объект класса ios содержит указатель на связанный с ним объект streambuf.
Классы istream и ostream являются производными от класса ios, в них определены функции, выполняющие ввод (istream) и вывод (ostream) данных базовых типов и строк.
В классе istream определены операции бесформатного ввода (без преобразования вводимых данных)и операции форматного ввода с преобразованием из внешнего представления во внутреннее.
Функции get() читают из потока данные типа char в массив, определяемый первым параметром, второй параметр задает максимальное число вводимых символов (l), третий параметр устанавливает символ-ограничитель, ограничивающий ввод. За последним введенным символом в массив пишется символ ‘\0’.
Функции read() выполняют чтение из потока и занесение в массив указанного числа символов
Для ввода строки, заканчивающейся символом-ограничителем, служит функция getline() при этом символ-ограничитель также заносится в массив
Для извлечения из потока данных типа char вплоть до символа-ограничителя служит вариант функции get():
Другие варианты функции get() обеспечивают извлечение одного символа
Пропуск символов с остановкой по ограничителю выполняет функция
Форматный ввод релизуется на основе переопределения операций ввода
Извлечение из istream и вставка в объект типа streambuf выполняет оператор-функция
Макрос FAR управляет способом представления указателей, макрос RTLENTRY определяет применение стандартной билиотеки времени выполнения.
продолжение
–PAGE_BREAK–