©Павловская Т.А. Язык С++ Курс «С++. Программирование на языке высокого уровня» Кобылинский Т.Г.
©Павловская Т.А. Язык С++ Лекция 6. Наследование. Шаблоны классов Простое и множественное наследование классов. Виртуальные методы. Абстрактные классы. Создание и использование шаблонов классов.
©Павловская Т.А. (СПбГУ ИТМО) Наследование Наследование является мощнейшим инструментом ООП и применяется для следующих взаимосвязанных целей: исключения из программы повторяющихся фрагментов кода; упрощения модификации программы; упрощения создания новых программ на основе существующих. Кроме того, наследование является единственной возможностью использовать объекты, исходный код которых недоступен, но в которые требуется внести изменения.
©Павловская Т.А. (СПбГУ ИТМО) Синтаксис наследования class имя : [private | protected | public] базовый_класс { тело класса }; class A {... }; class B {... }; class C {... }; class D: A, protected B, public C {... }; Ключи доступа
©Павловская Т.А. (СПбГУ ИТМО) В наследнике можно описывать новые поля и методы и переопределять существующие методы. Переопределять методы можно несколькими способами. Если какой-либо метод в потомке должен работать совершенно по-другому, чем в предке, метод описывается в потомке заново. При этом он может иметь другой набор аргументов. Если требуется внести добавления в метод предка, то в соответствующем методе потомка наряду с описанием дополнительных действий выполняется вызов метода предка с помощью операции доступа к области видимости. Если в программе планируется работать одновременно с различными типами объектов иерархии или планируется добавление в иерархию новых объектов, метод объявляется как виртуальный с помощью ключевого слова virtual. Все виртуальные методы иерархии с одним и тем же именем должны иметь одинаковый список аргументов.
©Павловская Т.А. (СПбГУ ИТМО) Правила наследования Ключ доступа Спецификатор в базовом классе Доступ в производном классе private нет protectedprivate publicprivate protectedprivateнет protected publicprotected publicprivateнет protected public
©Павловская Т.А. (СПбГУ ИТМО) private элементы базового класса в производном классе недоступны вне зависимости от ключа. Обращение к ним может осуществляться только через методы базового класса. Элементы protected при наследовании с ключом private становятся в производном классе private, в остальных случаях права доступа к ним не изменяются. Доступ к элементам public при наследовании становится соответствующим ключу доступа. Иными словами:
©Павловская Т.А. (СПбГУ ИТМО) Если базовый класс наследуется с ключом private, можно выборочно сделать некоторые его элементы доступными в производном классе: class Base{... public: void f(); }; class Derived : private Base{... public: Base::void f(); };
©Павловская Т.А. (СПбГУ ИТМО) Простое наследование class daemon : public monstr{ int brain; public: // Конструкторы: daemon(int br = 10){brain = br;}; daemon(color sk) : monstr (sk) {brain = 10;} daemon(char * nam) : monstr (nam) {brain = 10;} daemon(daemon &M) : monstr (M) {brain = M.brain;} Если конструктор базового класса требует указания параметров, он должен быть явным образом вызван в конструкторе производного класса в списке инициализации
©Павловская Т.А. (СПбГУ ИТМО) Конструкторы не наследуются, поэтому производный класс должен иметь собственные конструкторы. Порядок вызова конструкторов: Если в конструкторе потомка явный вызов конструктора предка отсутствует, автоматически вызывается конст-руктор предка по умолчанию. Для иерархии, состоящей из нескольких уровней, конструкторы предков вызываются начиная с самого верхнего уровня. После этого выполняются конструкторы тех элементов класса, которые являются объектами, в порядке их объявления в классе, а затем исполняется конструктор класса. В случае нескольких предков их конструкторы вызываются в порядке объявления. Порядок вызова конструкторов
©Павловская Т.А. (СПбГУ ИТМО) const daemon& operator = (daemon &M){ if (&M == this) return *this; brain = M.brain; monstr::operator = (M); return *this; Поля, унаследованные из класса monstr, недоступны функциям производного класса, поскольку они определены в базовом классе как private. Производный класс может не только дополнять, но и корректировать поведение базового класса. Переопределять в производном классе рекомендуется только виртуальные методы Операция присваивания
©Павловская Т.А. (СПбГУ ИТМО) Наследование деструкторов Деструкторы не наследуются. Если деструктор в производном классе не описан, он формируется автоматически и вызывает деструкторы всех базовых классов. В деструкторе производного класса не требуется явно вызывать деструкторы базовых классов, это будет сделано автоматически. Для иерархии, состоящей из нескольких уровней, деструкторы вызываются в порядке, строго обратном вызову конструкторов: сначала вызывается деструктор класса, затем деструкторы элементов класса, а потом деструктор базового класса.
©Павловская Т.А. (СПбГУ ИТМО) Раннее связывание Описывается указатель на базовый класс: monstr *p; Указатель ссылается на объект производного класса: p = new daemon; Вызов методов объекта происходит в соответствии с типом указателя, а не фактическим типом объекта: p->draw(1, 1, 1, 1); // Метод monstr Можно использовать явное преобразование типа указателя: (daemon * p)->draw(1, 1, 1, 1);
©Павловская Т.А. (СПбГУ ИТМО) Виртуальные методы virtual void draw(int x, int y, int scale, int position); vtbl vptr monstr *r, *p; // Создается объект класса monstr r = new monstr; // Создается объект класса daemon p = new daemon; // Вызывается метод monstr::draw r->draw(1, 1, 1, 1); // Вызывается метод daemon::draw p->draw(1, 1, 1, 1); //Обход механизма виртуальных методов p-> monstr::draw(1, 1, 1, 1);
©Павловская Т.А. (СПбГУ ИТМО) Описание и использование виртуальных методов Если в предке метод определен как виртуальный, метод, определенный в потомке с тем же именем и набором параметров, автоматически становится виртуальным, а с отличающимся набором параметров обычным. Виртуальные методы наследуются, то есть переопределять их в потомке требуется только при необходимости задать отличающиеся действия. Права доступа при переопределении изменить нельзя. Если виртуальный метод переопределен в потомке, объекты этого класса могут получить доступ к методу предка с помощью операции доступа к области видимости. Виртуальный метод не может объявляться с модификатором static, но может быть объявлен как дружественный. Если в классе вводится объявление виртуального метода, он должен быть определен хотя бы как чисто виртуальный.
©Павловская Т.А. (СПбГУ ИТМО) - содержит признак = 0 вместо тела: virtual void f(int) = 0; - должен переопределяться в производном классе. Класс, содержащий хотя бы один чисто виртуальный метод, называется абстрактным. абстрактный класс нельзя использовать при явном приведении типов, для описания типа параметра и типа возвращаемого функцией значения; допускается объявлять указатели и ссылки на абстрактный класс, если при инициализации не требуется создавать временный объект; если класс, производный от абстрактного, не определяет все чисто виртуальные функции, он также является абстрактным. Чисто виртуальные методы
©Павловская Т.А. (СПбГУ ИТМО) Множественное наследование class monstr{ public: int get_health();... }; class hero{ public: int get_health();... }; class ostrich: public monstr, public hero {... }; int main(){ ostrich A; cout << A.monstr::get_health(); cout << A.hero::get_health(); }
©Павловская Т.А. (СПбГУ ИТМО) class monstr{... }; class daemon: virtual public monstr{... }; class lady: virtual public monstr{... }; class baby: public daemon, public lady{... }; monstr daemonlady baby
©Павловская Т.А. (СПбГУ ИТМО) Множественное наследование применяется для того, чтобы обеспечить производный класс свойствами двух или более базовых. Чаще всего один из этих классов является основным, а другие обеспечивают некоторые дополнительные свойства, поэтому они называются классами подмешивания. По возможности классы подмешивания должны быть виртуальными и создаваться с помощью конструкторов без параметров, что позволяет избежать многих проблем, возникающих, когда у базовых классов есть общий предок. Рекомендации
©Павловская Т.А. (СПбГУ ИТМО) Виды отношений между классами ассоциация (два класса концептуально взаимодействуют друг с другом); наследование (отношение обобщения, «is a»); агрегация (отношение целое/часть, «has a»); строгая (композиция) нестрогая (по ссылке) зависимость (отношение использования)
©Павловская Т.А. (СПбГУ ИТМО) Шаблоны классов Параметризованный класс создает семейство родственных классов, которые можно применять к любому типу данных, передаваемому в качестве параметра. Наиболее широкое применение шаблоны находят при создании контейнерных классов. Создание шаблонов классов template определение_класса; Параметры шаблона перечисляются через запятую. В качестве параметров могут использоваться типы, шаблоны и переменные.
©Павловская Т.А. (СПбГУ ИТМО) Типы могут быть как стандартными, так и определенными пользователем. Для их описания используется ключевое слово class. Внутри шаблона параметр типа может применяться в любом месте, где допустимо использовать спецификацию типа, например: template class List{ class Node{ public: Data d; Node *next; Node *prev; Node(Data dat = 0){ d = dat; next = 0; prev = 0; } }; Параметры шаблона - типы
©Павловская Т.А. (СПбГУ ИТМО) Для любых параметров шаблона могут быть заданы значения по умолчанию, например: template class mar { /*...*/ }; template class C = mar> class Map{ C key; C value;... }; Параметр можно использовать при описании следующих за ним, например: template class X { /*... */ }; Значения параметров по умолчанию
©Павловская Т.А. (СПбГУ ИТМО) Cинтаксис описания методов шаблона на примере: template void List ::print() { /* тело функции */ } Здесь описание параметра шаблона, void тип возвращаемого функцией значения, List имя класса, параметр шаблона, print имя функции без параметров. Методы шаблона класса автоматически становятся шаблонами функций. Если метод описывается вне шаблона, его заголовок должен иметь следующие элементы: template возвр_тип имя_класса :: имя_функции (список_параметров функции) Методы шаблона класса
©Павловская Т.А. (СПбГУ ИТМО) Правила описания шаблонов Локальные классы не могут содержать шаблоны в качестве своих элементов. Шаблоны методов не могут быть виртуальными. Шаблоны классов могут содержать статические элементы, дружественные функции и классы. Шаблоны могут быть производными как от шаблонов, так и от обычных классов, а также являться базовыми и для шаблонов, и для обычных классов. Внутри шаблона нельзя определять friend-шаблоны.
©Павловская Т.А. (СПбГУ ИТМО) Переменные могут быть целого или перечисляемого типа, а также указателями или ссылками на объект или функцию. В теле шаблона они могут применяться в любом месте, где допустимо использовать константное выражение: template class Block{ public: Block(){p = new Type [kol];} ~Block(){delete [] p;} operator Type *(); protected: Type * p; }; template Block :: operator Type *(){ return p;} Параметры шаблона - переменные
©Павловская Т.А. (СПбГУ ИТМО) Пример параметра–указателя void f1() { cout << "I am f1()." << endl; } void f2() { cout << "I am f2()." << endl; } template struct A { void Show() { pf();} }; int main() { A aa; aa.Show();// вывод: I am f1(). A ab; ab.Show();// вывод: I am f2(). return 0; }
©Павловская Т.А. (СПбГУ ИТМО) Использование шаблонов классов При описании объекта после имени шаблона в угловых скобках перечисляются его аргументы: имя_шаблона имя_объекта [(параметры_конструктора)]; Аргументы должны соответствовать параметрам шаблона. Имя шаблона вместе с аргументами можно воспринимать как уточненное имя класса. Примеры создания объектов по шаблонам, описанным в предыдущем разделе: List List_int; List List_double; List List_monstr; Block buf; Block stado;
©Павловская Т.А. (СПбГУ ИТМО) При использовании параметров шаблона по умолчанию список аргументов может оказаться пустым, при этом угловые скобки опускать нельзя: template 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_monstr.add(i); List_monstr.print(); Использование шаблонов классов
©Павловская Т.А. (СПбГУ ИТМО) Для упрощения использования шаблонов классов можно применить переименование типов с помощью typedef : typedef List Ldbl; Ldbl List_double;
©Павловская Т.А. (СПбГУ ИТМО) Организация исходного кода Принято размещать все определение шаблонного класса в заголовочном файле и подключать его к нужным файлам с помощью директивы #include. Для предотвращения повторного включения этого файла используйте «стражи включения» // Point.h #ifndef POINT_H #define POINT_H template class Point { public: Point(T _x = 0, T _y = 0) : x(_x), y(_y) {} void Show() const; private: T x, y; }; template void Point ::Show() const { cout << " (" << x << ", " << y << ")" << endl; } #endif /* POINT_H */
©Павловская Т.А. (СПбГУ ИТМО) // Main.cpp #include #include "Point.h" using namespace std; int main() { Point p1; // 1 Point p2(7.32, -2.6); // 2 p1.Show(); p2.Show(); Point p3(13, 15); // 3 Point p4(17, 21); // 4 p3.Show(); p4.Show(); return 0; }
©Павловская Т.А. (СПбГУ ИТМО) Специализация шаблонов классов Для специализации метода требуется определить вариант его кода, указав в заголовке конкретный тип данных. Например, если заголовок обобщенного метода print шаблона List имеет вид template void List ::print(); специализированный метод для вывода списка символов будет выглядеть следующим образом: void List ::print(){ // Тело специализированного варианта метода print } -методов -классов -спец-я всего класса -частичная специализация
©Павловская Т.А. (СПбГУ ИТМО) Специализация всего класса // общий шаблон template class Sample { bool Less(T) const; /*...*/ }; // специализация для char* template <> class Sample { bool Less(char*) const; /*...*/ }; При специализации целого класса после описания обобщенного варианта класса помещается полное описание специализированного класса, при этом требуется заново определить все его методы. template class Pair { /*...*/ }; // специализация, где для T2 установлен тип int template class Pair { /*...*/ }; Если шаблонный класс имеет несколько параметров, то возможна частичная специализация:
©Павловская Т.А. (СПбГУ ИТМО) Использование классов функциональных объектов для настройки шаблонных классов template struct LessThan { bool operator() (const T& x, const T& y) { return x < y; } }; template struct GreaterThan { bool operator() (const T& x, const T& y) { return x > y; } }; template class PairSelect { public: PairSelect(const T& x, const T& y) : a(x), b(y) {} void OutSelect() const { cout << (Compare()(a, b) ? a : b) << endl; } private: T a, b; };
©Павловская Т.А. (СПбГУ ИТМО) int main() { PairSelect > ps1(13, 9); ps1.OutSelect(); // вывод: 9 PairSelect > ps2(13.8, 9.2); ps2.OutSelect(); // вывод: 13.8 return 0; }
©Павловская Т.А. (СПбГУ ИТМО) Шаблоны представляют собой мощное и эффективное средство обращения с различными типами данных, которое можно назвать параметрическим полиморфизмом, а также обеспечивают безопасное использование типов, в отличие от макросов препроцессора. Программа, использующая шаблоны, содержит полный код для каждого порожденного типа, что может увеличить размер исполняемого файла. С некоторыми типами данных шаблоны могут работать не так эффективно, как с другими. В этом случае имеет смысл использовать специализацию шаблона. Достоинства и недостатки шаблонов