ВНИМАНИЕ
Если конструктор базового класса требует указания параметров, он должен быть явным образом вызван в конструкторе производного класса в списке инициализации (это продемонстрировано в трех последних конструкторах).
Не наследуется и операция присваивания, поэтому ее также требуется явно определить в классе daemon. Обратите внимание на запись функции-операции: в ее теле применен явный вызов функции-операции присваивания из базового класса. Чтобы лучше представить себе синтаксис вызова, ключевое слово operator вместе со знаком операции можно интерпретировать как имя функции-операции.
Вызов функций базового класса предпочтительнее копирования фрагментов кода из функций базового класса в функции производного. Кроме сокращения объема кода, этим достигается упрощение модификации программы: изменения требуется вносить только в одну точку программы, что сокращает количество возможных ошибок.
Правила для деструкторов при наследовании:
- Деструкторы не наследуются, и если программист не описал в производном классе деструктор, он формируется по умолчанию и вызывает деструкторы всех базовых классов.
- В отличие от конструкторов, при написании деструктора производного класса в нем не требуется явно вызывать деструкторы базовых классов, поскольку это будет сделано автоматически.
- Для иерархии классов, состоящей из нескольких уровней, деструкторы вызываются в порядке, строго обратном вызову конструкторов: сначала вызывается деструктор класса, затем - деструкторы элементов класса, а потом деструктор базового класса.
Поля, унаследованные из класса monster, недоступны функциям производного класса, поскольку они определены в базовом классе как private. Если функциям, определенным в daemon, требуется работать с этими полями, можно либо описать их в базовом классе как protected, либо обращаться к ним с помощью функций из monster, либо явно переопределить их в daemon так, как было показано в предыдущем разделе.
Добавляемые поля в наследнике могут совпадать и по имени, и по типу с полями базового класса. При этом поле предка будет скрыто.
Статические поля, объявленные в базовом классе, наследуются обычным образом. Все объекты базового класса и всех его наследников разделяют единственную копию статических полей базового класса.
Рассматривая наследование методов, обратите внимание на то, что в классе daemon описан метод draw, переопределяющий метод с тем же именем в классе monster (поскольку отрисовка различных персонажей, естественно, выполняется по-разному). Таким образом, производный класс может не только дополнять, но и корректировать поведение базового класса. Доступ к переопределенному методу базового класса для производного класса выполняется через уточненное с помощью операции доступа к области видимости имя.
Класс-потомок наследует все методы базового класса, кроме конструкторов, деструктора и операции присваивания. Не наследуются ни дружественные функции, ни дружественные отношения классов.
В классе-наследнике можно определять новые методы. В них разрешается вызывать любые доступные методы базового класса. Если имя метода в наследнике совпадает с именем метода базового класса, то метод производного класса скрывает все методы базового класса с таким именем. При этом прототипы методов могут не совпадать. Если в методе-наследнике требуется вызвать одноименный метод родительского класса, нужно задать его с префиксом класса. Это же касается и статических методов.
Виртуальные методы
Работа с объектами чаще всего производится через указатели. Указателю на базовый класс можно присвоить значение адреса объекта любого производного класса, например:
monster *p; // Описывается указатель на базовый классp = new daemon; /* Указатель ссылается на объект производного класса */ Вызов методов объекта происходит в соответствии с типом указателя, а не фактическим типом объекта, на который он ссылается, поэтому при выполнении оператора, например,
p -> draw(1, 1, 1, 1); будет вызван метод класса monster, а не класса daemon, поскольку ссылки на методы разрешаются во время компоновки программы. Этот процесс называется ранним связыванием. Чтобы вызвать метод класса daemon, можно использовать явное преобразование типа указателя:
((daemon * p)) -> draw(1, 1, 1, 1); Это не всегда возможно, поскольку в разное время указатель может ссылаться на объекты разных классов иерархии, и во время компиляции программы конкретный класс может быть неизвестен.
В качестве примера можно привести функцию, параметром которой является указатель на объект базового класса. На его место во время выполнения программы может быть передан указатель любого производного класса. Другой пример - связный список указателей на различные объекты иерархии, с которым требуется работать единообразно.
В С++ реализован механизм позднего связывания, когда разрешение ссылок на функцию происходит на этапе выполнения программы в зависимости от конкретного типа объекта, вызвавшего функцию. Этот механизм реализован с помощью виртуальных методов.
Для определения виртуального метода используется спецификатор virtual:
virtual void draw(int x, int y, int scale, int position); Рассмотрим правила использования виртуальных методов.
|
Шаблоны классов Шаблон класса позволяет задать класс, параметризованный типом данных. Передача классу различных типов данных в качестве параметра создает семейство родственных классов. Наиболее широкое применение шаблоны находят при создании контейнерных классов. Контейнерным называется класс, который предназначен для хранения каким-либо образом организованных данных и работы с ними. Преимущество использования шаблонов состоит в том, что как только алгоритм работы с данными определен и отлажен, он может применяться к любым типам данных без переписывания кода. Создание шаблонов классов Рассмотрим процесс создания шаблона класса на примере двусвязного списка. Поскольку списки часто применяются для организации данных, удобно описать список в виде класса, а так как может потребоваться хранить данные различных типов, этот класс должен быть параметризованным. Сначала рассмотрим непараметризованную версию класса. Список состоит из узлов, связанных между собой с помощью указателей. Каждый узел хранит целое число, являющееся ключом списка. Опишем вспомогательный класс для представления одного узла списка: class Node { public: int d; // Данные Node *next, *prev; //Указатели на предыдущий и последующий узлы Node(int dat = 0) { d = dat; next = 0; prev = 0; } // Конструктор }; Поскольку этот класс будет описан внутри класса, представляющего список, поля для простоты доступа из внешнего класса сделаны доступными (public). Это позволяет обойтись без функций доступа и изменения полей. Назовем класс списка List: class List { class Node{ ... }; Node *pbeg, *pend; // Указатели на начало и конец списка public: List() { pbeg = 0; pend = 0; } // Конструктор ~List(); // Деструктор void add(int d); // Добавление узла в конец списка Node * find(int i); // Поиск узла по ключу Node * insert(int key, int d); /* Вставка узла d после узла с ключом key */ bool remove(int key); // Удаление узла void print(); // Печать списка в прямом направлении void print_back(); // Печать списка в обратном направлении }; Рассмотрим реализацию методов класса. Метод add выделяет память под новый объект типа Node и присоединяет его к списку, обновляя указатели на его начало и конец: void List::add(int d) { Node *pv = new Node(d); // Выделение памяти под новый узел if (pbeg == 0)pbeg = pend = pv; // Первый узел списка else { pv->prev = pend; // Связывание нового узла с предыдущим pend->next = pv; pend = pv; } // Обновление указателя на конец списка } Метод find выполняет поиск узла с заданным ключом и возвращает указатель на него в случае успешного поиска и 0 в случае отсутствия такого узла в списке: Node * List::find( int d ) { Node *pv = pbeg; while (pv) { if(pv->d == d)break; pv=pv->next; } return pv; } Метод insert вставляет в список узел после узла с ключом key и возвращает указатель на вставленный узел. Если такого узла в списке нет, вставка не выполняется и возвращается значение 0: Node * List::insert(int key, int d) { if(Node *pkey = find(key)) { // Поиск узла с ключом key /* Выделение памяти под новый узел и его инициализация */ Node *pv = new Node(d); /* Установление связи нового узла с последующим */ pv->next = pkey->next; // Установление связи нового узла с предыдущим pv->prev = pkey; // Установление связи предыдущего узла с новым pkey->next = pv; if( pkey != pend) (pv->next)->prev = pv; /* Установление связи последующего узла с новым */ /* Обновление указателя на конец списка, если узел вставляется в конец */ else pend = pv; return pv; } return 0; } Метод remove удаляет узел с заданным ключом из списка и возвращает значение true в случае успешного удаления и false, если узел с таким ключом в списке не найден: bool List::remove(int key) { if(Node *pkey = find(key)) { if (pkey == pbeg) { // Удаление из начала списка pbeg = pbeg->next; pbeg->prev = 0; } else if (pkey == pend) { // Удаление из конца списка pend = pend->prev; pend->next = 0; } else { // Удаление из середины списка (pkey->prev)->next = pkey->next; (pkey->next)->prev = pkey->prev; } delete pkey; return true;} return false;} Методы печати списка в прямом и обратном направлении поэлементно просматривают список, переходя по соответствующим ссылкам: void List::print(){ Node *pv = pbeg; cout << endl << "list: "; while (pv){ cout << pv->d << ' '; pv=pv->next;} cout << endl; } void List::print_back(){ Node *pv = pend; cout << endl << " list back: "; while (pv){ cout << pv->d << ' '; pv=pv->prev;} cout << endl;} Деструктор списка освобождает память из-под всех его элементов: List::~List(){ if (pbeg != 0){ Node *pv = pbeg; while (pv) {pv = pv->next; delete pbeg; pbeg = pv;} }} Ниже приведен пример программы, использующей класс List. Программа формирует список из 5 чисел, выводит его на экран, добавляет число в список, удаляет число из списка и снова выводит его на экран: int main() { List L; for (int i = 2; i<6; i++) L.add(i); L.print(); L.print_back(); L.insert(2,200); if (!L.remove(5))cout << "not found"; L.print(); L.print_back();} Класс List предназначен для хранения целых чисел. Чтобы хранить в нем данные любого типа, требуется описать этот класс как шаблон и передать тип в качестве параметра. Синтаксис описания шаблона: template <описание_параметров_шаблона> class имя { /* определение класса */ }; Шаблон класса начинается с ключевого слова template. В угловых скобках записывают параметры шаблона. При использовании шаблона на место этих параметров шаблону передаются аргументы: типы и константы, перечисленные через запятую. Типы могут быть как стандартными, так и определенными пользователем. Для их описания в списке параметров используется ключевое слово class. В простейшем случае одного параметра это выглядит как <class T>. Здесь T является параметром-типом. Имя параметра может быть любым, но принято начинать его с префикса T. Внутри класса-шаблона параметр может появляться в тех местах, где разрешается указывать конкретный тип, например: template <class TData> class List { class Node{ public: TData d; Node *next; Node *prev; Node(TData dat = 0){d = dat; next = 0; prev = 0;} }; ... } Класс TData можно рассматривать как параметр, на место которого при компиляции будет подставлен конкретный тип данных. Получившийся шаблонный класс имеет тип List<TData>. Методы шаблона класса автоматически становятся шаблонами функций. Если метод описывается вне шаблона, его заголовок должен иметь следующие элементы: template <описание_параметров_шаблона> возвр_тип имя_класса <параметры_шаблона >:: имя_функции (список_параметров функции) Проще рассмотреть синтаксис описания методов шаблона на примере: template <class Data> void List <Data>::print() { /* тело функции */ } Описание параметров шаблона в заголовке функции должно соответствовать шаблону класса. Локальные классы не могут иметь шаблоны в качестве своих элементов. Шаблоны методов не могут быть виртуальными. Шаблоны классов могут содержать статические элементы, дружественные функции и классы. Шаблоны могут быть производными как от шаблонов, так и от обычных классов, а также являться базовыми и для шаблонов, и для обычных классов. Внутри шаблона нельзя определять friend-шаблоны. Если у шаблона несколько параметров, они перечисляются через запятую. Ключевое слово class требуется записывать перед каждым параметром, например: template <class T1, class T2> struct Pair { T1 first; T2 second; }; Параметрам шаблонного класса можно присваивать значения по умолчанию, они записываются после знака "=". Как и для обычных функций, задавать значения по умолчанию следует, начиная с правых параметров. Ниже приведено полное описание параметризованного класса двусвязного списка List. template <class TData> class List { class Node { public: TData d; Node *next, *prev; Node(TData dat = 0){d = dat; next = 0; prev = 0;} }; Node *pbeg, *pend; public: List(){pbeg = 0; pend = 0;} ~List(); void add(TData d); Node * find(TData i); Node * insert(TData key, TData d); bool remove(TData key); void print(); void print_back();}; //------------------------- template <class TData> List <TData>::~List() { if (pbeg !=0) { Node *pv = pbeg; while (pv) {pv = pv->next; delete pbeg; pbeg = pv;} } } //------------------------- template <class TData> void List <TData>::print() { Node *pv = pbeg; cout << endl << "list: "; while (pv) { cout << pv->d << ' '; pv = pv->next; } cout << endl; } //------------------------- template <class TData> void List <TData>::print_back() { Node *pv = pend; cout << endl << " list back: "; while (pv) { cout << pv->d << ' '; pv = pv->prev; } cout << endl; } //------------------------- template <class TData> void List <TData>::add(TData d) { Node *pv = new Node(d); if (pbeg == 0)pbeg = pend = pv; else { pv->prev = pend; pend->next = pv; pend = pv; } } //------------------------- template <class TData> Node * List <TData>::find( TData d) { Node *pv = pbeg; while (pv) { if(pv->d == d)break; pv = pv->next; } return pv; } //------------------------- template <class TData> Node * List <TData>::insert(TData key, TData d) { if(Node *pkey = find(key)) { Node *pv = new Node(d); pv->next = pkey->next; pv->prev = pkey; pkey->next = pv; if( pkey != pend)(pv->next)->prev = pv; else pend = pv; return pv; } return 0; } //------------------------- template <class TData> bool List <TData>::remove(TData key) { if(Node *pkey = find(key)) { if (pkey == pbeg) { pbeg = pbeg->next; pbeg->prev = 0; } else if (pkey == pend) { pend = pend->prev; pend->next = 0; } else { (pkey->prev)->next = pkey->next; (pkey->next)->prev = pkey->prev; } delete pkey; return true; } return false; } Если требуется использовать шаблон List для хранения данных не встроенного, а определенного пользователем типа, в его описание необходимо добавить перегрузку операции вывода в поток и сравнения на равенство, а если для его полей используется динамическое выделение памяти, то и операцию присваивания. При определении синтаксиса шаблона было сказано, что в него, кроме типов, могут передаваться константы. Соответствующим параметром шаблона может быть: переменная целого, символьного, булевского или перечислимого типа; указатель на объект или указатель на функцию; ссылка на объект или ссылка на функцию; указатель на элемент класса. В теле шаблона такие параметры могут применяться в любом месте, где допустимо использовать константное выражение. В качестве примера создадим шаблон класса, содержащего блок памяти определенной длины и типа: template <class Type, int kol> class Block { public: Block(){p = new Type [kol];} ~Block(){delete [] p;} operator Type *(); protected: Type * p; }; template <class Type, int kol> Block <Type, kol>:: operator Type *() { return p; } У класса-шаблона могут быть друзья, и шаблоны тоже могут быть друзьями. Класс может быть объявлен внутри шаблона, а шаблон - внутри как класса, так и шаблона. Единственным ограничением является то, что шаблонный класс нельзя объявлять внутри функции. В любом классе, как в обычном, так и в шаблоне, можно объявить метод-шаблон. После создания и отладки шаблоны классов удобно помещать в заголовочные файлы. |
Использование шаблонов классов
Чтобы создать при помощи шаблона конкретный объект конкретного класса, при описании объекта после имени шаблона в угловых скобках перечисляются его фактические параметры:
имя_шаблона <фактические параметры> имя_объекта (параметры_конструктора);
Процесс создания конкретного класса из шаблона путем подстановки аргументов называется инстанцированием шаблона. Имя шаблона вместе с фактическими параметрами можно воспринимать как уточненное имя класса. Примеры создания объектов по шаблонам:
List <int> List_int; // список целых чиселList <double> List_double; // список вещественных чиселList <monster> List_monster; // список объектов класса monsterBlock <char, 128> buf; // блок символовBlock <monstr, 100> stado; // блок объектов класса monsterPair<int, int> a; // объявление пары целыхPair<int, double> b; // объявление пары "целый, вещественный"Pair<int, double> b = { 1, 2.1 }; // объявление с инициализациейPair<string, Date> d; // аргументы - пользовательские классыПри использовании параметров шаблона по умолчанию список аргументов может оказаться пустым, при этом угловые скобки опускать нельзя:
template<class T = char> class String;String<>* p;Для каждого инстанцированного класса компилятор создает имя, отличающееся и от имени шаблона, и от имен других инстанцированных классов. Тем самым каждый инстанцированный класс определяет отдельный тип. В классе-шаблоне разрешено объявлять статические методы и статические поля, однако следует учесть, что каждый инстанцированный класс обладает собственной копией статических элементов.
После создания объектов с ними можно работать так же, как с объектами обычных классов, например:
for (int i = 1; i<10; i++)List_double.add(i*0.08);List_double.print();//----------------------------------for (int i = 1; i<10; i++)List_monster.add(i);List_monster.print();//----------------------------------strcpy(buf, "Очень важное сообщение");cout << buf << endl;Для упрощения использования шаблонов классов можно применить переименование типов с помощью typedef: