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

Лекция 07 - Композиция, наследование и полиморфизм.pptx.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); } CCircle можно использовать везде, где используется CShape Указатель на производный класс проводится к CShape * p. Shape = &circle; указателю на базовый

Пример неправильного использования публичного наследования 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 public: CDerived: public CBase Защищенное CDerived: protected Сравнение типов наследования в C++ Публичное CBase public: CDerived: public CBase Защищенное CDerived: protected CBase Закрытое CDerived : private CBase public: protected: private: недоступно Public, private & protected

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

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

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

Пример class CEmployee { public: std: : string Get. Name()const { return m_name; } Пример 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() Конструктор класса CEmployee (служащий) объявлен защищенным, чтобы не допустить бессмысленное создание абстрактных «служащих» (на работу берут конкретных специалистов) 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 {

Порядок вызова деструкторов ● В 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[])

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

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

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

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

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

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

Пример 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; double Get. Area()const=0; void Draw()const=0; Пример class IShape { public: virtual }; void Transform()=0; double Get. Area()const=0; void Draw()const=0; ~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 {}; 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[]) {

Пример 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"; }

Не злоупотребляйте использованием 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 CCat : public CMammal { public: virtual std: : string Get. Type()const {return "cat"; } }; class CDog : public CMammal { public: virtual std: : string Get. Type()const {return "dog"; } 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 CText : public IDrawable { public: virtual void Draw()const; }; class CLine : public IShape { public: virtual void Draw()const; // класс объектов, имеющих заливку class CFillable { public: void Set. Fill. Color(int fill. Color); int Get. Fill. Color()const; virtual ~CFillable(){} private: int m_fill. Color; }; class CRectangle : public IShape, public CFillable { public: virtual void Draw()const; };

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

Ромбовидное наследование CAnimal CWinged. Anim al CMammal CBat Ромбовидное наследование CAnimal CWinged. Anim al CMammal 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(); // Животное с крыльями class CWinged. Animal : public CAnimal { public: virtual void Fly(){} }; // как ест летучая мышь: // как млекопитающее? bat. CMammal: : Eat(); // или как крылатое животное? bat. CWinged. Animal: : Eat(); return 0;

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

Пример использования виртуального наследования // Животное 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++ (первая по силе – отношение дружбы) ● Объявляя один класс наследником другого, мы подписываем с родительским классом своеобразный контракт, которому обязаны неукоснительно следовать ● ● Изменения в родительском класса могут оказать влияние на всех его потомков Никогда не злоупотребляйте созданием многоуровневых иерархий наследования