Скачать презентацию Композиция Что такое композиция Композиция агрегирование включение Скачать презентацию Композиция Что такое композиция Композиция агрегирование включение

Лекция 07 - Композиция, наследование и полиморфизм.pptx

  • Количество слайдов: 75

Композиция Композиция

Что такое композиция? Композиция (агрегирование, включение) – простейший механизм для создания нового класса путем Что такое композиция? Композиция (агрегирование, включение) – простейший механизм для создания нового класса путем объединения нескольких объектов существующих классов в единое целое При агрегировании между классами действует «отношение принадлежности» У машины есть кузов, колеса и двигатель У человека есть голова, руки, ноги и тело У треугольника есть вершины Вложенные объекты обычно объявляются закрытыми (private) внутри класса-агрегата

Пример 1 - Треугольник class CPoint { public: CPoint(double x, double y); Точка double Пример 1 - Треугольник class CPoint { public: CPoint(double x, double y); Точка double Get. X()const; double Get. Y()const; private: double m_x, m_y; }; Треугольник class CTriangle { public: CTriangle(CPoint const& p 1, CPoint const& p 2, CPoint const& p 3); CPoint Get. Vertex(unsigned index)const; private: CPoint m_p 1, m_p 2, m_p 3; };

Пример 2 - Автомобиль // Колесо class CWheel {. . . }; // Кузов Пример 2 - Автомобиль // Колесо class CWheel {. . . }; // Кузов class CBody {. . . }; // Двигатель class CEngine {. . . }; // Автомобиль class CAutomobile { public: . . . private: CBody m_body; CEngine m_engine; CWheel m_wheels[4]; };

Пример 3 - Презентация // Слайд class CSlide {. . . }; // Слайды Пример 3 - Презентация // Слайд class CSlide {. . . }; // Слайды class CSlides { public: CSlide & operator[](unsigned index); CSlide const & operator[](unsigned index)const; . . . private: std: : vector m_items; }; // Презентация class CPresentation { public: CSlides & Get. Slides(); CSlides const& Get. Slides()const; private: CSlides m_slides; };

Наследование Наследование

Что такое наследование? Важнейший механизм ООП, позволяющий описать новый класс на основе уже существующего Что такое наследование? Важнейший механизм ООП, позволяющий описать новый класс на основе уже существующего При наследовании свойства и функциональность родительского класса наследуются новым классом Класс-наследник имеет доступ к публичным и защищенным методам и полям класса родительского класса Класс-наследник может добавлять свои данные и методы, а также переопределять методы базового класса

Терминология Родительский или базовый класс (класс-родитель) – класс, выступающий в качестве основы при наследовании Терминология Родительский или базовый класс (класс-родитель) – класс, выступающий в качестве основы при наследовании Класс-потомок (дочерний класс, класс-наследник) – класс, образованный в результате наследования от родительского класса Иерархия наследования – отношения между родительским классом и его потомками Интерфейс класса – совокупность публичных методов класса, доступная для использования вне класса В интерфейсной части данные обычно не размещают Реализация класса – совокупность приватных методов и данных класса

Графическое изображение иерархий наследования Животное Рыба Родительский класс Птица Орел Классы-потомки Голубь Классы-потомки Графическое изображение иерархий наследования Животное Рыба Родительский класс Птица Орел Классы-потомки Голубь Классы-потомки

Варианты наследования По типу наследования Публичное (открытое) наследование Приватное (закрытое) наследование Защищенное наследование По Варианты наследования По типу наследования Публичное (открытое) наследование Приватное (закрытое) наследование Защищенное наследование По количеству базовых классов Одиночное наследование (один базовый класс) Множественное наследование (два и более базовых классов)

Открытое наследование Открытое наследование

Публичное (открытое) наследование Публичное наследование – это наследование интерфейса (наследование типа) При публичном наследовании Публичное (открытое) наследование Публичное наследование – это наследование интерфейса (наследование типа) При публичном наследовании открытые (публичные) поля и методы родительского класса остаются открытыми Производный класс является подтипом родительского Производный класс служит примером отношения «является» (is a) Производный класс является объектом родительского Примеры: «Собака является животным» , «Прямоугольник является замкнутой фигурой»

Пример – иерархия в человеческом обществе class CPerson { public: std: : string Get. Пример – иерархия в человеческом обществе class CPerson { public: std: : string Get. Name()const; std: : string Get. Address()const; int Get. Birth. Year()const; private: }; class CStudent : public CPerson { public: std: : string Get. University. Name()const; std: : string Get. Group. Name()const; unsigned Get. Grade()const; // год обучения }; class CWorker : public CPerson { public: std: : string Get. Job. Position()const; int Get. Experience()const; }; CPerson CStudent CWorker

Публичное наследование как наследование интерфейса При публичном наследовании класс-потомок наследует интерфейс родителя С объектами Публичное наследование как наследование интерфейса При публичном наследовании класс-потомок наследует интерфейс родителя С объектами класса-наследника можно обращаться так же как с объектами базового класса Если это не так, то, вероятно открытое наследование использовать не следует Указатели и ссылки на класс-потомок могут приводиться к указателям и ссылкам на базовый класс

Пример публичного наследования – иерархия фигур CShape C 2 DShape CCircle CTriangle C 3 Пример публичного наследования – иерархия фигур CShape C 2 DShape CCircle CTriangle C 3 DShape CCube CSphere void Process. Shape(CShape & shape) {. . . } void Test() { CCircle circle; Process. Shape(circle); CShape * p. Shape = &circle; } CCircle можно использовать везде, где используется CShape Указатель на производный класс проводится к указателю на базовый

Пример неправильного использования публичного наследования CPoint CCircle CCylinder Неправильный ход мыслей: «Окружность можно получить, Пример неправильного использования публичного наследования CPoint CCircle CCylinder Неправильный ход мыслей: «Окружность можно получить, добавив к точке радиус, а цилиндр – добавив к окружности высоту» Неправильный контекст использования открытого наследования: Открытое наследование должно использоваться не для того, чтобы производный класс мог использовать код базового для реализации своей функциональности Класс-наследник должен представлять собой частный случай более общей абстрации Здесь: Окружность не является частным случаем точки Цилиндр не является частным случаем окружности, и, тем более, точки

Закрытое наследование Закрытое наследование

Приватное (закрытое) наследование Приватное наследование – это наследование реализации При приватном наследовании открытые и Приватное (закрытое) наследование Приватное наследование – это наследование реализации При приватном наследовании открытые и защищенные поля и методы родительского класса становятся закрытыми полями и методами производного Производный класс напрямую не поддерживает открытый интерфейс базового, но пользуется его реализацией, предоставляя собственный открытый интерфейс Производный класс служит примером отношения «реализован на основе» (implemented as) Производный класс реализован на основе родительского Примеры: «Класс Stack реализован на основе класса Array»

Пример – стек целых чисел class CInt. Array { public: int operator[](int index)const; int& Пример – стек целых чисел class CInt. Array { public: int operator[](int index)const; int& operator[](int index); int Get. Length()const; void Insert. Item(int index, int value); private: . . . }; class CInt. Stack : private CInt. Array { public: void Push(int element); int Pop(); bool Is. Empty()const; }; Нельзя использовать открытое наследование • Стек не является массивом, но пользуется реализацией массива • К стеку не применимы операции индексированного доступа

Композиция – предпочтительная альтернатива приватному наследованию Вместо наследования реализации во многих случаях может оказаться Композиция – предпочтительная альтернатива приватному наследованию Вместо наследования реализации во многих случаях может оказаться лучше использовать композицию При композиции новый класс может использовать несколько экземпляров существующего класса Композиция делает классы менее зависимым друг от друга, чем наследование Возможны исключения, когда приватное наследование является более предпочтительным Необходимо получить доступ к защищенным методам существующего класса С точки зрения интерфейса нового класса – различий нет никаких

Пример class CInt. Array { public: int operator[](int index)const; int& operator[](int index); int Get. Пример class CInt. Array { public: int operator[](int index)const; int& operator[](int index); int Get. Length()const; void Insert. Item(int index, int value); private: . . . }; class CInt. Stack 2 { public: void Push(int element); int Pop(); bool Is. Empty()const; private: CInt. Array m_items; };

Защищенное наследование Защищенное наследование

Защищенное наследование – наследование реализации, доступной для последующего наследования При защищенном наследовании открытые поля Защищенное наследование – наследование реализации, доступной для последующего наследования При защищенном наследовании открытые поля и методы родительского класса становятся защищенными полями и методами производного Данные методы могут использоваться классами, порожденными от производного Как и в случае закрытого наследования порожденный класс должен предоставить собственный интерфейс

Пример class CInt. Array { public: int operator[](int index)const; int& operator[](int index); int Get. Пример class CInt. Array { public: int operator[](int index)const; int& operator[](int index); int Get. Length()const; void Insert. Item(int index, int value); }; class CInt. Stack : protected CInt. Array { public: void Push(int element); int Pop()const; bool Is. Empty()const; }; class CInt. Stack. Ex : public CInt. Stack { public: int Get. Number. Of. Elements()const; };

Различия между защищенным и открытым наследованием При защищенном наследовании публичные и защищенные поля родительского Различия между защищенным и открытым наследованием При защищенном наследовании публичные и защищенные поля родительского класса являются защищенными и доступны его «внукам» - классам, унаследованным от производного класса При закрытом наследовании – они доступны только самому производному классу Разницу между защищенным и закрытым наследованием почувствуют лишь наследники производного класса

Сравнение типов наследования Сравнение типов наследования

Сравнение типов наследования в C++ Публичное CBase CDerived: public CBase public: Защищенное CDerived: protected Сравнение типов наследования в C++ Публичное CBase CDerived: public CBase public: Защищенное CDerived: protected CBase Закрытое CDerived : private CBase public: protected: private: недоступно Public, private & protected

Типы наследования в других языках программирования Публичное наследование является наиболее естественным вариантом наследования и Типы наследования в других языках программирования Публичное наследование является наиболее естественным вариантом наследования и поддерживается всеми ОО языками программирования Другие типы наследования являются, скорее, экзотикой, т. к. практически всегда можно обойтись без них Вместо приватного наследования используют композицию Защищенное наследование – в большинстве случаев не имеет смысла

Вызов конструкторов и деструкторов при наследовании Вызов конструкторов и деструкторов при наследовании

Порядок вызова конструкторов В C++ при конструировании экземпляра классанаследника всегда происходит предварительный вызов конструктора Порядок вызова конструкторов В C++ при конструировании экземпляра классанаследника всегда происходит предварительный вызов конструктора базового класса В C++ вызов конструктора базового класса происходит до инициализации полей класса наследника Конструктор класса-наследника может явно передать конструктору базового класса необходимы параметры при помощи списка инициализации Если вызов конструктора родительского класса не указан явно в списке инициализации, компилятор пытается вызвать конструктор по умолчанию класса-родителя

Пример Конструктор класса CEmployee (служащий) объявлен защищенным, чтобы не допустить бессмысленное создание абстрактных «служащих» Пример Конструктор класса CEmployee (служащий) объявлен защищенным, чтобы не допустить бессмысленное создание абстрактных «служащих» (на работу берут конкретных специалистов) class CEmployee { public: std: : string Get. Name()const { return m_name; } protected: CEmployee(std: : string const& name) : m_name(name) { std: : cout << "CEmployee() " << name << "n"; } private: std: : string m_name; }; int main(int argc, char * argv[]) { CProgrammer programmer("Bill Gates", C_PLUS); return 0; } Output: CEmployee() Bill Gates CProgrammer() enum Programming. Language { C_PLUS, C_SHARP, VB_NET, }; class CProgrammer : public CEmployee { public: CProgrammer(std: : string const& name, Programming. Language language) : CEmployee(name) , m_language(language) { std: : cout << "CProgrammer()n"; } Programming. Language Get. Language()const { return m_language; } private: Programming. Language m_language; };

Порядок вызова деструкторов В C++ порядок вызова деструкторов всегда обратен порядку вызова конструкторов сначала Порядок вызова деструкторов В C++ порядок вызова деструкторов всегда обратен порядку вызова конструкторов сначала вызывается деструктор класса-наследника, затем деструктор базового класса и т. д. вверх по иерархии классов

Пример class CTable { public: CTable(std: : string const& db. File. Name) { m_table. Пример class CTable { public: CTable(std: : string const& db. File. Name) { m_table. File. Open(db. File. Name); std: : cout << "Table constructedn"; } virtual ~CTable() { m_table. File. Close(); std: : cout << "Table destroyedn"; } private: CFile m_table. File; }; Output: Table constructed Indexed table created Indexed table destroyed Table destroyed class CIndexed. Table : public CTable { public: CIndexed. Table(std: : string const& db. File. Name, std: : string const& index. File. Name) : CTable(db. File. Name) { m_index. File. Open(index. File. Name); std: : cout << "Indexed table createdn"; } ~CIndexed. Table() { m_index. File. Close(); std: : cout << "Indexed table destroyedn"; } private: CFile m_index. File; }; int main(int argc, char * argv[]) { CIndexed. Table table("users. dat", "users. idx"); return 0; }

Перегрузка методов в классе-наследнике Перегрузка методов в классе-наследнике

Перегрузка методов в классе наследнике В C++ метод производного класса замещает собой все методы Перегрузка методов в классе наследнике В C++ метод производного класса замещает собой все методы родительского класса с тем же именем Количество и типы аргументов значения не имеют Для вызова метода родительского класса из метода класса наследника используется метод Base: :

Пример class CBase { public: void Print() { std: : cout << Пример class CBase { public: void Print() { std: : cout << "CBase: : Printn"; } void Print(std: : string const& param) { std: : cout << "CBase: : Print " << param << "n"; } }; int main(int argc, char * argv[]) { CDerived derived; // вызов метода Print() наследника derived. Print("test"); std: : cout << "===n"; // вызов метода Print() базового класса derived. CBase: : Print(); std: : cout << "===n“; // вызов метода Print базового класса class CDerived : public CBase derived. CBase: : Print("test 1"); { public: return 0; void Print(std: : string const& param) } { CBase: : Print(param); Output: std: : cout << "CDerived: : Print " << param << "n"; CBase: : Print test } CDerived: : Print test }; === CBase: : Print test 1

Виртуальные функции Виртуальные функции

Задача – иерархия геометрических фигур Рассмотрим следующую иерархию геометрических фигур: CShape – базовый класс Задача – иерархия геометрических фигур Рассмотрим следующую иерархию геометрических фигур: CShape – базовый класс «фигура» CCircle – класс, моделирующий окружность CRectangle - класс, моделирующий прямоугольник Каждая фигура обладает следующими свойствами: Имя: «Shape» , «Circle» либо «Rectangle» Площадь фигуры

class CShape { public: std: : string Get. Type()const{return class CShape { public: std: : string Get. Type()const{return "Shape"; } double Get. Area()const{return 0; } }; class CRectangle : public CShape { public: CRectangle(double width, double height) : m_width(width), m_height(height){} std: : string Get. Type()const{return "Rectangle"; } double Get. Area()const{ return m_width * m_height; } private: double m_width; double m_height; }; class CCircle : public CShape { public: CCircle(double radius): m_radius(radius){} std: : string Get. Type()const{return "Circle"; } double Get. Area()const{return 3. 14159265 * m_radius; } private: double m_radius; };

Так, вроде, все работает: int main(int argc, char * argv[]) { CCircle circle(10); CRectangle Так, вроде, все работает: int main(int argc, char * argv[]) { CCircle circle(10); CRectangle rectangle(20, 10); std: : cout << "Circle area: " << circle. Get. Area() << "n"; std: : cout << "Rectangle area: " << rectangle. Get. Area() << "n"; return 0; } Output: Circle area: 314. 159 Rectangle area: 200

А вот так - нет void Print. Shape. Area(CShape const& shape) { std: : А вот так - нет void Print. Shape. Area(CShape const& shape) { std: : cout << shape. Get. Type() << " area: " << shape. Get. Area() << "n"; } int main(int argc, char * argv[]) { CCircle circle(10); CRectangle rectangle(20, 10); Print. Shape. Area(circle); Print. Shape. Area(rectangle); return 0; } Output: Shape area: 0

В чем же проблема? Проблема в том, что в данной ситуации при выборе вызываемых В чем же проблема? Проблема в том, что в данной ситуации при выборе вызываемых методов компилятор руководствуется типом ссылки или указателя В нашем случае происходит вызов методов класса CShape, т. к. функция Print. Shape. Area принимает ссылку данного типа Методы, при вызове которых необходимо руководствоваться типом объекта, должны быть объявлены виртуальными

Виртуальные методы Метод класса может быть объявлен виртуальным, если допускается его альтернативная реализация в Виртуальные методы Метод класса может быть объявлен виртуальным, если допускается его альтернативная реализация в порожденном классе При вызове виртуальной функции через указатель или ссылку на объект базового класса будет вызвана реализация данной функции, специфичная для фактического типа объекта Виртуальные функции обозначаются в объявлении класса при помощи ключевого слова virtual Виртуальные функции позволяют использовать полиморфизм Полиморфизм позволяет осуществлять работу с разными реализациями через один и тот же интерфейс

class CShape { public: virtual std: : string Get. Type()const{return class CShape { public: virtual std: : string Get. Type()const{return "Shape"; } virtual double Get. Area()const{return 0; } }; class CRectangle : public CShape { public: CRectangle(double width, double height) : m_width(width), m_height(height){} virtual std: : string Get. Type()const{return "Rectangle"; } virtual double Get. Area()const{ return m_width * m_height; } private: double m_width; double m_height; }; class CCircle : public CShape { public: CCircle(double radius): m_radius(radius){} virtual std: : string Get. Type()const{return "Circle"; } virtual double Get. Area()const{return 3. 14159265 * m_radius; } private: double m_radius; };

Теперь заработало как надо void Print. Shape. Area(CShape const& shape) { std: : cout Теперь заработало как надо void Print. Shape. Area(CShape const& shape) { std: : cout << shape. Get. Type() << " area: " << shape. Get. Area() << "n"; } int main(int argc, char * argv[]) { CCircle circle(10); CRectangle rectangle(20, 10); Print. Shape. Area(circle); Print. Shape. Area(rectangle); return 0; } Output: Circle area: 314. 159 Rectangle area: 200

Особенности реализации виртуальных функций в C++ В C++ функции, объявленные в базовом классе виртуальными, Особенности реализации виртуальных функций в C++ В C++ функции, объявленные в базовом классе виртуальными, остаются виртуальными в классахпотомках Использовать слово virtual в классах наследниках не обязательно (хотя и желательно) В C++ виртуальные функции не являются виртуальными, если они вызваны в конструкторе или деструкторе данного класса Такое поведение специфично для механизма инициализации и разрушения объектов в C++; в других языках программирования может быть по-другому

Виртуальный деструктор Деструктор класса, имеющего наследников, всегда должен явно объявляться виртуальным Это обеспечивает корректный Виртуальный деструктор Деструктор класса, имеющего наследников, всегда должен явно объявляться виртуальным Это обеспечивает корректный вызов деструктора нужного класса при вызове оператора delete с указателем на базовый класс Деструктор, не объявленный явно виртуальным, а также автоматически сгенерированный деструктор является не виртуальным Классы без виртуальных деструкторов не предназначены для расширения Классы стандартных коллекций STL (строки, векторы) не имеют виртуальных деструкторов, поэтому наследоваться от них нельзя

Проблемы при использовании невиртуального деструктора class CBase { public: CBase(): m_p. Base. Data(new char Проблемы при использовании невиртуального деструктора class CBase { public: CBase(): m_p. Base. Data(new char [100]) { std: : cout << "Base class data were createdn"; } ~CBase() { delete [] m_p. Base. Data; std: : cout << "Base class data were deletedn"; } private: char * m_p. Base. Data; }; class CDerived : public CBase { public: CDerived(): m_p. Derived. Data(new char [1000]) { std: : cout << "Derived class data were createdn"; } ~CDerived() { delete [] m_p. Derived. Data; std: : cout << "Derived class data were deletedn"; } private: char * m_p. Derived. Data; }; int main(int argc, char * argv[]) { { CDerived derived; } std: : cout << "===n"; CDerived * p. Derived = new CDerived(); // этот объект удалится нормально delete p. Derived; p. Derived = NULL; std: : cout << "===n"; CBase * p. Base = new CDerived(); /* а вот тут будет вызван лишь деструктор базового класса */ delete p. Base; p. Base = NULL; return 0; Output: } Base class data were created Derived class data were deleted Base class data were deleted === Base class data were created Derived class data were created Base class data were deleted

Исправляем проблему, объявив деструктор виртуальным class CBase { public: CBase(): m_p. Base. Data(new char Исправляем проблему, объявив деструктор виртуальным class CBase { public: CBase(): m_p. Base. Data(new char [100]) { std: : cout << "Base class data were createdn"; } virtual ~CBase() { delete [] m_p. Base. Data; std: : cout << "Base class data were deletedn"; } private: char * m_p. Base. Data; }; class CDerived : public CBase { public: CDerived(): m_p. Derived. Data(new char [1000]) { std: : cout << "Derived class data were createdn"; } ~CDerived() { delete [] m_p. Derived. Data; std: : cout << "Derived class data were deletedn"; } private: char * m_p. Derived. Data; }; int main(int argc, char * argv[]) { { CDerived derived; } std: : cout << "===n"; CDerived * p. Derived = new CDerived(); // этот объект удалится нормально delete p. Derived; p. Derived = NULL; std: : cout << "===n"; CBase * p. Base = new CDerived(); /* а вот тут будет вызван лишь деструктор базового класса */ delete p. Base; p. Base = NULL; return 0; Output: } Base class data were created Derived class data were deleted Base class data were deleted === Base class data were created Derived class data were deleted Base class data were deleted

Подводим итоги Всегда используем виртуальный деструктор: В базовых классах В классах, от которых возможно Подводим итоги Всегда используем виртуальный деструктор: В базовых классах В классах, от которых возможно наследование в будущем Например, в классах с виртуальными методами Не используем виртуальные деструкторы В классах, от которых не планируется создавать производные классы в будущем Также возможно в базовом классе объявить защищенный невиртуальный деструктор Объекты данного удалить напрямую невозможно – только через указатель на класс-наследник Данный деструктор будет доступен классам-наследникам

Абстрактные классы Абстрактные классы

Абстрактные классы Возможны ситуации, когда базовый класс представляет собой абстрактное понятие, и выступает лишь Абстрактные классы Возможны ситуации, когда базовый класс представляет собой абстрактное понятие, и выступает лишь как базовый класс (интерфейс) для производных классов Невозможно дать осмысленное определение его виртуальных функций Какова площадь объекта «CShape» , как его нарисовать? Такие виртуальные функции следует объявлять чисто виртуальными (pure virtual), добавив инициализатор =0, опустив тело функции Класс является абстрактным, если в нем содержится хотя бы одна чисто виртуальная функция, либо он не реализует хотя бы одну чисто виртуальную функцию своего родителя Экземпляр абстрактного класса создать невозможно

Пример class CShape { public: virtual std: : string Get. Type()const=0; virtual double Get. Пример class CShape { public: virtual std: : string Get. Type()const=0; virtual double Get. Area()const=0; virtual void Draw()const=0; };

Интерфейс Невозможно создать экземпляр абстрактного класса Все методы абстрактного класса должны быть реализованы в Интерфейс Невозможно создать экземпляр абстрактного класса Все методы абстрактного класса должны быть реализованы в производных классах Абстрактный класс, содержащий только чисто виртуальные методы еще называют интерфейсом Деструктор такого класса обязательно должен быть виртуальным (не обязательно чисто виртуальным) В некоторых ОО языках программирования для объявления интерфейсов могут существовать отдельные конструкции языка Ключевое слово interface в Java/C#/Action. Script

Пример class IShape { public: virtual void Transform()=0; virtual double Get. Area()const=0; virtual void Пример class IShape { public: virtual void Transform()=0; virtual double Get. Area()const=0; virtual void Draw()const=0; virtual ~IShape(){} }; class CRectangle : public IShape { public: virtual void Transform() {. . . } virtual double Get. Area()const {. . . } virtual void Draw()const {. . . } } class CCircle : public IShape { public: virtual void Transform() {. . . } virtual double Get. Area()const {. . . } virtual void Draw()const {. . . } }

Приведение типов вверх и вниз по иерархии классов Приведение типов вверх и вниз по иерархии классов

Приведение типов в пределах иерархии классов Приведение типов вверх по иерархии всегда возможно и Приведение типов в пределах иерархии классов Приведение типов вверх по иерархии всегда возможно и может происходить неявно Всякая собака является животным Всякий ястреб является птицей Исключение – ромбовидное множественное наследование Приведение типов вниз по иерархии не всегда возможно Не всякое млекопитающее – собака, но некоторые млекопитающие могут быть собаками В C++ для такого приведения типов используется оператор dynamic_cast Приведение типа между несвязанными классами иерархии недопустимо Собаки не являются птицами Кошка – не ястреб и не собака Ястреб – не млекопитающее Животное Млекопитающее Собака Кошка Птица Ястреб

Оператор dynamic_cast Оператор приведения типа dynamic_cast позволяет выполнить безопасное приведение ссылки или указателя на Оператор dynamic_cast Оператор приведения типа dynamic_cast позволяет выполнить безопасное приведение ссылки или указателя на один тип данных к другому Проверка допустимости приведения типа осуществляется во время выполнения программы При невозможности приведения типа будет возвращен нулевой указатель (при приведении типа указателя) или сгенерировано исключение типа std: : bad_cast (при приведении типа ссылки) Для осуществления проверок времени выполнения используется информация о типах (RTTI – Run-Time Type Information) RTTI требует, чтобы в классе имелся хотя бы один виртуальный метод (хотя бы деструкор)

Пример 1 – иерархия животных class CAnimal { public: virtual ~CAnimal() {} }; class Пример 1 – иерархия животных class CAnimal { public: virtual ~CAnimal() {} }; class CBird : public CAnimal {}; class CEagle : public CBird {}; class CMammal : public CAnimal {}; class CDog : public CMammal {}; class CCat : public CMammal {}; void Print. Animal. Type(CAnimal const * p. Animal) { if (dynamic_cast(p. Animal) != NULL) std: : cout << "dogn"; else if (dynamic_cast(p. Animal) != NULL) std: : cout << "catn"; else if (dynamic_cast(p. Animal) != NULL) std: : cout << "eaglen"; else if (dynamic_cast(p. Animal) != NULL) std: : cout << "some unknown type of mammalsn"; else if (dynamic_cast(p. Animal) != NULL) std: : cout << "some unknown type birdsn"; else std: : cout << "some unknown type of animalsn"; } int main(int argc, char* argv[]) { CDog dog; Print. Animal. Type(&dog); CAnimal * p. Animal = new CCat(); Print. Animal. Type(p. Animal); delete p. Animal; return 0; }

Пример 2 – приведение ссылок CMammal const& Make. Mammal(CAnimal const & animal) { return Пример 2 – приведение ссылок CMammal const& Make. Mammal(CAnimal const & animal) { return dynamic_cast(animal); } int main(int argc, char* argv[]) { CDog dog; CMammal const& dog. As. Mammal = Make. Mammal(dog); CCat cat; // неявное приведение типов вверх по иерархии Cat -> Animal CAnimal const& cat. As. Animal = cat; CMammal const& animal. As. Mammal = Make. Mammal(cat. As. Animal); CEagle eagle; try { CMammal const& eagle. As. Mammal = Make. Mammal(eagle); } catch(std: : bad_cast const& error) { std: : cout << error. what() << "n"; } return 0; }

Не злоупотребляйте использованием dynamic_cast Везде, где это можно, следует обходиться без использования данного оператора, Не злоупотребляйте использованием dynamic_cast Везде, где это можно, следует обходиться без использования данного оператора, отдавая предпочтение виртуальным (или чисто виртуальным функциям) В противном случае при добавлении нового класса в иерархию может понадобиться провести ревизию всего кода, использующего dynamic_cast При использовании виртуальных функций ничего особенного делать не надо

Решение без dynamic_cast class CAnimal { public: virtual std: : string Get. Type()const = Решение без dynamic_cast class CAnimal { public: virtual std: : string Get. Type()const = 0; virtual ~CAnimal(){} }; // птицы и млекопитающие – абстрактные понятия // поэтому в них реализовывать Get. Type() нет смысла class CBird : public CAnimal{}; class CMammal : public CAnimal{}; class CEagle : public CBird { public: virtual std: : string Get. Type()const {return "eagle"; } }; class CDog : public CMammal { public: virtual std: : string Get. Type()const {return "dog"; } }; class CCat : public CMammal { public: virtual std: : string Get. Type()const {return "cat"; } }; void Print. Animal. Type(CAnimal const & animal) { std: : cout << animal. Get. Type() << "n"; }

Множественное наследование Множественное наследование

Множественное наследование Язык C++ допускает наследование класса от более, чем одного базового класса Такое Множественное наследование Язык C++ допускает наследование класса от более, чем одного базового класса Такое наследование называют множественным При этом порожденный класс может обладать свойствами сразу нескольких родительских классов Например, класс может реализовывать сразу несколько интерфейсов или использвоать несколько реализаций

Пример иерархии классов IDrawable CFillable IShape CRectangle CText CLine Пример иерархии классов IDrawable CFillable IShape CRectangle CText CLine

Пример // интерфейс объектов, которые можно нарисовать class IDrawable { public: virtual void Draw()const Пример // интерфейс объектов, которые можно нарисовать class IDrawable { public: virtual void Draw()const = 0; virtual ~IDrawable(){} }; // интерфейс геометрических фигур class IShape : public IDrawable { }; // класс объектов, имеющих заливку class CFillable { public: void Set. Fill. Color(int fill. Color) ; int Get. Fill. Color()const; virtual ~CFillable(){} private: int m_fill. Color; }; class CText : public IDrawable { public: virtual void Draw()const ; }; class CLine : public IShape { public: virtual void Draw()const ; }; class CRectangle : public IShape, public CFillable { public: virtual void Draw()const ; };

Проблемы, возникающие при множественном наследовании При всей своей мощности и гибкости множественное наследование может Проблемы, возникающие при множественном наследовании При всей своей мощности и гибкости множественное наследование может явиться источником проблем Ярким примером является т. н. «ромбовидное наследование» (родительские классы объекта наследуются от одного базового класса) В некоторых ЯП множественное наследование запрещено Порождаемый класс может наследоваться только от одного базового класса и реализовывать несколько интерфейсов – множественное интерфейсное наследование

Ромбовидное наследование CAnimal CMammal CWinged. Animal CBat Ромбовидное наследование CAnimal CMammal CWinged. Animal CBat

Пример проблемы ромбовидного наследования // Животное class CAnimal { public: virtual void Eat(){} }; Пример проблемы ромбовидного наследования // Животное class CAnimal { public: virtual void Eat(){} }; // Летучая мышь class CBat : public CMammal , public CWinged. Animal { }; // Млекопитающее class CMammal : public CAnimal { public: virtual void Feed. With. Milk(){} }; int main(int argc, char * argv[]) { CBat bat; // error: ambiguous access of 'Eat' bat. Eat(); // как ест летучая мышь: // как млекопитающее? bat. CMammal: : Eat(); // или как крылатое животное? bat. CWinged. Animal: : Eat(); // Животное с крыльями class CWinged. Animal : public CAnimal { public: virtual void Fly(){} }; return 0; }

Возможное решение данной проблемы виртуальное наследование Проблема ромбовидного наследования заключается в том, что класс Возможное решение данной проблемы виртуальное наследование Проблема ромбовидного наследования заключается в том, что класс CBat содержит в себе две копии данных объекта CAnimal Копия, унаследованная от CMammal Копия, унаследованная от CWinged. Animal Виртуальное наследование в ряде случаев позволяет решить проблемы неоднозначности, возникающие при множественном наследовании При виртуальном наследовании происходит объединение нескольких унаследованных экземпляров общего предка в один Базовый класс, наследуемый множественно, определяется виртуальным при помощи ключевого слова virtual

Пример использования виртуального наследования // Животное class CAnimal { public: virtual void Eat(){} }; Пример использования виртуального наследования // Животное class CAnimal { public: virtual void Eat(){} }; // Летучая мышь class CBat : public CMammal , public CWinged. Animal { }; // Млекопитающее class CMammal : public virtual CAnimal { public: virtual void Feed. With. Milk(){} }; int main(int argc, char * argv[]) { CBat bat; // Теперь нормально bat. Eat(); return 0; } // Животное с крыльями class CWinged. Animal : public virtual CAnimal { public: virtual void Fly(){} };

Ограничения виртуального наследования Классы-предки не могут одновременно перегружать одни и те же методы своего Ограничения виртуального наследования Классы-предки не могут одновременно перегружать одни и те же методы своего родителя В нашем случае – нельзя переопределять метод Eat() одновременно и в CMammal, и в CWinged. Animal – будет ошибка компиляции В случае переопределения этого метода в одном из классов компилятор выдаст предупреждение

Когда множественное наследование может быть полезным При аккуратном использовании множественное наследование может быть весьма Когда множественное наследование может быть полезным При аккуратном использовании множественное наследование может быть весьма эффективным Создание класса, использующего несколько реализаций Широко применяется в библиотеках ATL и WTL Создание класса, реализующего несколько интерфейсов Основное правило – избегайте ромбовидного наследования

Преимущества использования наследования Возможность создания новых типов, расширяя или используя функционал уже имеющихся Возможность Преимущества использования наследования Возможность создания новых типов, расширяя или используя функционал уже имеющихся Возможность существования нескольких реализаций одного и того же интерфейса Абстракция Замена операторов множественного выбора полиморфизмом

Наследование и вопросы проектирования Наследование – вторая по силе взаимосвязь между классами в C++ Наследование и вопросы проектирования Наследование – вторая по силе взаимосвязь между классами в C++ (первая по силе – отношение дружбы) Объявляя один класс наследником другого, мы подписываем с родительским классом своеобразный контракт, которому обязаны неукоснительно следовать Изменения в родительском класса могут оказать влияние на всех его потомков Никогда не злоупотребляйте созданием многоуровневых иерархий наследования