Внимание. Если конструктор базового класса требует указания параметров, он должен быть явным образом вызван в конструкторе производного класса в списке инициализации

Если конструктор базового класса требует указания параметров, он должен быть явным образом вызван в конструкторе производного класса в списке инициализации (это продемонстрировано в трех последних конструкторах).

Не наследуется и операция присваивания, поэтому ее также требуется явно определить в классе 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); Рассмотрим правила использования виртуальных методов.
  • Если в базовом классе метод определен как виртуальный, метод, определенный в производном классе с тем же именем и набором параметров, автоматически становится виртуальным, а с отличающимся набором параметров - обычным.
  • Виртуальные методы наследуются, то есть переопределять их в производном классе требуется только при необходимости задать отличающиеся действия. Права доступа при переопределении изменить нельзя.
  • Если виртуальный метод переопределен в производном классе, объекты этого класса могут получить доступ к методу базового класса с помощью операции доступа к области видимости.
  • Виртуальный метод не может объявляться с модификатором static, но может быть объявлен как дружественная функция.
  • Если производный класс содержит виртуальные методы, они должны быть определены в базовом классе хотя бы как чисто виртуальные.
Чисто виртуальный метод содержит признак = 0 вместо тела, например: virtual void f(int) = 0; Чисто виртуальный метод должен переопределяться в производном классе (возможно, опять как чисто виртуальный). Если определить метод draw в классе monster как виртуальный, решение о том, метод какого класса вызвать, будет приниматься в зависимости от типа объекта, на который ссылается указатель: monster *r, *p;r = new monster; // Создается объект класса monsterp = new daemon; // Создается объект класса daemonr -> draw(1,1,1,1); // Вызывается метод monster::drawp -> draw(1,1,1,1); // Вызывается метод daemon::drawp -> monster::draw(1,1,1,1); // Обход механизма виртуальных методов Если объект класса daemon будет вызывать метод draw не непосредственно, а косвенно (то есть из другого метода, который может быть определен только в классе monster), будет вызван метод draw класса daemon. Итак, виртуальным называется метод, ссылка на который разрешается на этапе выполнения программы (перевод красивого английского слова virtual - всего-навсего "фактический", то есть ссылка разрешается по факту вызова). Рассмотрим механизм позднего связывания. Для каждого класса (не объекта!), содержащего хотя бы один виртуальный метод, компилятор создает таблицу виртуальных методов (vtbl), в которой для каждого виртуального метода записан его адрес в памяти. Адреса методов содержатся в таблице в порядке их описания в классах. Адрес любого виртуального метода имеет в vtbl одно и то же смещение для каждого класса в пределах иерархии. Каждый объект содержит скрытое дополнительное поле ссылки на vtbl, называемое vptr. Оно заполняется конструктором при создании объекта (для этого компилятор добавляет в начало тела конструктора соответствующие инструкции). На этапе компиляции ссылки на виртуальные методы заменяются на обращения к vtbl через vptr объекта, а на этапе выполнения в момент обращения к методу его адрес выбирается из таблицы. Таким образом, вызов виртуального метода, в отличие от обычных методов и функций, выполняется через дополнительный этап получения адреса метода из таблицы. Это несколько замедляет выполнение программы, поэтому без необходимости делать методы виртуальными смысла не имеет. Рекомендуется делать виртуальными деструкторы для того, чтобы гарантировать правильное освобождение памяти из-под динамического объекта, поскольку в любой момент времени будет выбран деструктор, соответствующий фактическому типу объекта. Четкого правила, по которому метод следует делать виртуальным, не существует. Можно только дать рекомендацию объявлять виртуальными методы, для которых есть вероятность, что они будут переопределены в производных классах. Методы, которые во всей иерархии останутся неизменными или те, которыми производные классы пользоваться не будут, делать виртуальными нет смысла. С другой стороны, при проектировании иерархии не всегда можно предсказать, каким образом будут расширяться базовые классы, особенно при проектировании библиотек классов, а объявление метода виртуальным обеспечивает гибкость и возможность расширения. Для пояснения последнего тезиса представим себе, что вызов метода draw осуществляется из метода перемещения объекта. Если текст метода перемещения не зависит от типа перемещаемого объекта (поскольку принцип перемещения всех объектов одинаков, а для отрисовки вызывается конкретный метод), переопределять этот метод в производных классах нет необходимости, и он может быть описан как невиртуальный. Если метод draw виртуальный, метод перемещения сможет без перекомпиляции работать с объектами любых производных классов - даже тех, о которых при его написании ничего известно не было. Виртуальный механизм работает только при использовании указателей или ссылок на объекты. Объект, определенный через указатель или ссылку и содержащий виртуальные методы, называется полиморфным. В данном случае полиморфизм состоит в том, что с помощью одного и того же обращения к методу выполняются различные действия в зависимости от типа, на который ссылается указатель в каждый момент времени. Абстрактные классы Класс, содержащий хотя бы один чисто виртуальный метод, называется абстрактным. Абстрактные классы предназначены для представления общих понятий, которые предполагается конкретизировать в производных классах. Абстрактный класс может использоваться только в качестве базового для других классов - объекты абстрактного класса создавать нельзя, поскольку прямой или косвенный вызов чисто виртуального метода приводит к ошибке при выполнении. При определении абстрактного класса необходимо иметь в виду следующее:
  • Абстрактный класс нельзя использовать при явном приведении типов, для описания типа параметра и типа возвращаемого функцией значения;
  • Допускается объявлять указатели и ссылки на абстрактный класс, если при инициализации не требуется создавать временный объект.
  • Если класс, производный от абстрактного, не определяет все чисто виртуальные функции, он также является абстрактным.
Таким образом, можно создать функцию, параметром которой является указатель на абстрактный класс. На место этого параметра при выполнении программы может передаваться указатель на объект любого производного класса. Это позволяет создавать полиморфные функции, работающие с объектом любого типа в пределах одной иерархии. Множественное наследование Множественное наследование означает, что класс имеет несколько базовых классов. При этом, если в базовых классах есть одноименные элементы, может произойти конфликт идентификаторов, который устраняется с помощью операции доступа к области видимости: class monster{ public: int get_health();... };class hero{ public: int get_health();... };class being: public monster, public hero{... };int main(){ being A; cout << A.monster::get_health(); cout << A.hero::get_health();} Использование конструкции A.get_health() приведет к ошибке, поскольку компилятор не в состоянии разобраться, метод какого из базовых классов требуется вызвать. Если у базовых классов есть общий предок, это приведет к тому, что производный от этих базовых класс унаследует два экземпляра полей предка, что чаще всего является нежелательным. Чтобы избежать такой ситуации, требуется при наследовании общего предка определить его как виртуальный класс: class monster{... };class daemon: virtual public monster{... };class lady: virtual public monster{... };class god: public daemon, public lady{... }; Класс god содержит только один экземпляр полей класса monster. Альтернатива наследованию Наследование - это очень сложная тема, и даже создатель языка С++ Б. Страуструп рекомендует везде, где это возможно, обходиться без него. Альтернативой наследованию при проектировании классов является вложение, когда один класс включает в себя поля, являющиеся классами или указателями на них. Например, если есть класс "двигатель", а требуется описать класс "самолет", логично сделать двигатель полем этого класса, а не его предком. Вид вложения, когда в классе описано поле объектного типа, называют композицией. Если в классе описан указатель на объект другого класса, это обычно называют агрегацией. При композиции время жизни всех объектов (и объемлющего, и его полей) одинаково. Агрегация представляет собой более слабую связь между объектами, потому что объекты, на которые ссылаются поля-указатели, могут появляться и исчезать в течение жизни содержащего их объекта, кроме того, один и тот же указатель может ссылаться на объекты разных классов в пределах одной иерархии. Поле-указатель может также ссылаться не на один объект, а на неопределенное количество объектов, например быть указателем на начало линейного списка. Отличия структур и объединений от классов Структуры (struct) и объединения (union) представляют собой частные случаи классов. Структуры отличаются от классов тем, что доступ к элементам, а также базовый класс при наследовании по умолчанию считаются public. Структуры предпочтительнее использовать для объектов, все элементы которых доступны. Доступ в объединениях также устанавливается public, кроме того, в них вообще нельзя использовать спецификаторы доступа. Объединение не может участвовать в иерархии классов. Элементами объединения не могут быть объекты, содержащие конструкторы и деструкторы. Объединение может иметь конструктор и другие методы, только не статические. В анонимном объединении методы описывать нельзя.
Шаблоны классов Шаблон класса позволяет задать класс, параметризованный типом данных. Передача классу различных типов данных в качестве параметра создает семейство родственных классов. Наиболее широкое применение шаблоны находят при создании контейнерных классов. Контейнерным называется класс, который предназначен для хранения каким-либо образом организованных данных и работы с ними. Преимущество использования шаблонов состоит в том, что как только алгоритм работы с данными определен и отлажен, он может применяться к любым типам данных без переписывания кода. Создание шаблонов классов Рассмотрим процесс создания шаблона класса на примере двусвязного списка. Поскольку списки часто применяются для организации данных, удобно описать список в виде класса, а так как может потребоваться хранить данные различных типов, этот класс должен быть параметризованным. Сначала рассмотрим непараметризованную версию класса. Список состоит из узлов, связанных между собой с помощью указателей. Каждый узел хранит целое число, являющееся ключом списка. Опишем вспомогательный класс для представления одного узла списка: 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:

typedef List <double> Ldbl;Ldbl List_double;

Понравилась статья? Добавь ее в закладку (CTRL+D) и не забудь поделиться с друзьями:  



double arrow
Сейчас читают про: