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

0

No comments posted yet

Comments

Slide 1

Композиция

Slide 2

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

Slide 3

Пример 1 - Треугольник class CPoint { public: CPoint(double x, double y); double GetX()const; double GetY()const; private: double m_x, m_y; }; class CTriangle { public: CTriangle(CPoint const& p1, CPoint const& p2, CPoint const& p3); CPoint GetVertex(unsigned index)const; private: CPoint m_p1, m_p2, m_p3; };

Slide 4

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

Slide 5

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

Slide 6

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

Slide 7

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

Slide 8

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

Slide 9

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

Slide 10

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

Slide 11

Открытое (публичное) наследование

Slide 12

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

Slide 13

Пример – иерархия в человеческом обществе class CPerson { public: std::string GetName()const; std::string GetAddress()const; int GetBirthYear()const; private: }; class CStudent : public CPerson { public: std::string GetUniversityName()const; std::string GetGroupName()const; unsigned GetGrade()const; // год обучения }; class CWorker : public CPerson { public: std::string GetJobPosition()const; int GetExperience()const; }; CPerson CStudent CWorker

Slide 14

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

Slide 15

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

Slide 16

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

Slide 17

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

Slide 18

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

Slide 19

Пример – стек целых чисел class CIntArray { public: int operator[](int index)const; int& operator[](int index); int GetLength()const; void InsertItem(int index, int value); private: ... }; class CIntStack : private CIntArray { public: void Push(int element); int Pop(); bool IsEmpty()const; }; Нельзя использовать открытое наследование Стек не является массивом, но пользуется реализацией массива К стеку не применимы операции индексированного доступа

Slide 20

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

Slide 21

Пример class CIntArray { public: int operator[](int index)const; int& operator[](int index); int GetLength()const; void InsertItem(int index, int value); private: ... }; class CIntStack2 { public: void Push(int element); int Pop(); bool IsEmpty()const; private: CIntArray m_items; };

Slide 22

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

Slide 23

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

Slide 24

Пример class CIntArray { public: int operator[](int index)const; int& operator[](int index); int GetLength()const; void InsertItem(int index, int value); }; class CIntStack : protected CIntArray { public: void Push(int element); int Pop()const; bool IsEmpty()const; }; class CIntStackEx : public CIntStack { public: int GetNumberOfElements()const; };

Slide 25

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

Slide 26

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

Slide 27

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

Slide 28

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

Slide 29

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

Slide 30

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

Slide 31

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

Slide 32

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

Slide 33

Пример class CTable { public: CTable(std::string const& dbFileName) { m_tableFile.Open(dbFileName); std::cout << "Table constructed\n"; } virtual ~CTable() { m_tableFile.Close(); std::cout << "Table destroyed\n"; } private: CFile m_tableFile; }; class CIndexedTable : public CTable { public: CIndexedTable(std::string const& dbFileName, std::string const& indexFileName) :CTable(dbFileName) { m_indexFile.Open(indexFileName); std::cout << "Indexed table created\n"; } ~CIndexedTable() { m_indexFile.Close(); std::cout << "Indexed table destroyed\n"; } private: CFile m_indexFile; }; int main(int argc, char * argv[]) { CIndexedTable table("users.dat", "users.idx"); return 0; } Output: Table constructed Indexed table created Indexed table destroyed Table destroyed

Slide 34

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

Slide 35

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

Slide 36

Пример class CBase { public: void Print() { std::cout << "CBase::Print\n"; } void Print(std::string const& param) { std::cout << "CBase::Print " << param << "\n"; } }; class CDerived : public CBase { public: void Print(std::string const& param) { CBase::Print(param); std::cout << "CDerived::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 базового класса derived.CBase::Print("test1"); return 0; } Output: CBase::Print test CDerived::Print test === CBase::Print === CBase::Print test1

Slide 37

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

Slide 38

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

Slide 39

class CShape { public: std::string GetType()const{return "Shape";} double GetArea()const{return 0;} }; class CRectangle : public CShape { public: CRectangle(double width, double height) :m_width(width), m_height(height){} std::string GetType()const{return "Rectangle";} double GetArea()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 GetType()const{return "Circle";} double GetArea()const{return 3.14159265 * m_radius * m_radius;} private: double m_radius; };

Slide 40

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

Slide 41

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

Slide 42

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

Slide 43

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

Slide 44

class CShape { public: virtual std::string GetType()const{return "Shape";} virtual double GetArea()const{return 0;} }; class CRectangle : public CShape { public: CRectangle(double width, double height) :m_width(width), m_height(height){} virtual std::string GetType()const{return "Rectangle";} virtual double GetArea()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 GetType()const{return "Circle";} virtual double GetArea()const{return 3.14159265 * m_radius * m_radius;} private: double m_radius; };

Slide 45

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

Slide 46

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

Slide 47

class CBase { public: CBase() {SayHello();} ~CBase() {SayGoodbye();} void SayHelloAndGoodbye() {SayHello();SayGoodbye();} virtual void SayHello()const {cout << "Hello from CBase\n";} virtual void SayGoodbye()const {cout << "Goodbye from CBase\n";} }; class CDerived : public CBase { public: CDerived(){} ~CDerived(){} virtual void SayHello()const {cout << "Hello from CDerived\n";} virtual void SayGoodbye()const {cout << "Goodbye from CDerived\n";} }; int _tmain(int argc, _TCHAR* argv[]) { CDerived derived; derived.SayHelloAndGoodbye(); return 0; } Пример Output: Hello from CBase Hello from CDerived Goodbye from CDerived Goodbye from CBase

Slide 48

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

Slide 49

Проблемы при использовании невиртуального деструктора class CBase { public: CBase():m_pBaseData(new char [100]) { std::cout << "Base class data were created\n"; } ~CBase() { delete [] m_pBaseData; std::cout << "Base class data were deleted\n"; } private: char * m_pBaseData; }; class CDerived : public CBase { public: CDerived():m_pDerivedData(new char [1000]) { std::cout << "Derived class data were created\n"; } ~CDerived() { delete [] m_pDerivedData; std::cout << "Derived class data were deleted\n"; } private: char * m_pDerivedData; }; int main(int argc, char * argv[]) { { CDerived derived; } std::cout << "===\n"; CDerived * pDerived = new CDerived(); // этот объект удалится нормально delete pDerived; pDerived = NULL; std::cout << "===\n"; CBase * pBase = new CDerived(); /* а вот тут будет вызван лишь деструктор базового класса */ delete pBase; pBase = NULL; return 0; } Output: Base class data were created Derived class data were created Derived class data were deleted Base class data were deleted === Base class data were created Derived 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

Slide 50

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

Slide 51

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

Slide 52

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

Slide 53

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

Slide 54

Пример class CShape { public: virtual std::string GetType()const=0; virtual double GetArea()const=0; virtual void Draw()const=0; virtual ~CShape(){}; };

Slide 55

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

Slide 56

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

Slide 57

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

Slide 58

Пример class IDataStream { public: virtual bool IsEOF()const = 0; virtual char ReadChar() = 0; virtual ~IDataStream(){} }; class CFileDataStream : public IDataStream { public: CFileDataStream(FILE * pFile); virtual bool IsEOF()const; virtual char ReadChar(); private: FILE * m_pFile; }; class CMemoryDataStream : public IDataStream { public: CMemoryDataStream(std::vector<char> const& data); virtual bool IsEOF()const; virtual char ReadChar(); private: std::vector<char> const & m_data; size_t m_readPosition; }; std::string ReadDataFromStream(IDataStream & s) { std::string data; while (!s.IsEOF()) { data += s.ReadChar(); } return data; }

Slide 59

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

Slide 60

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

Slide 61

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

Slide 62

Пример 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 PrintAnimalType(CAnimal const * pAnimal) { if (dynamic_cast<CDog const*>(pAnimal) != nullptr) std::cout << "dog\n"; else if (dynamic_cast<CCat const*>(pAnimal) != nullptr) std::cout << "cat\n"; else if (dynamic_cast<CEagle const*>(pAnimal) != nullptr) std::cout << "eagle\n"; else if (dynamic_cast<CMammal const*>(pAnimal) != nullptr) std::cout << "some unknown type of mammals\n"; else if (dynamic_cast<CBird const*>(pAnimal) != nullptr) std::cout << "some unknown type birds\n"; else std::cout << "some unknown type of animals\n"; } int main(int argc, char* argv[]) { CDog dog; PrintAnimalType(&dog); CAnimal * pAnimal = new CCat(); PrintAnimalType(pAnimal); delete pAnimal; return 0; }

Slide 63

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

Slide 64

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

Slide 65

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

Slide 66

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

Slide 67

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

Slide 68

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

Slide 69

Пример // интерфейс объектов, которые можно нарисовать class IDrawable { public: virtual void Draw()const = 0; virtual ~IDrawable(){} }; // интерфейс геометрических фигур class IShape : public IDrawable { }; // класс объектов, имеющих заливку class CFillable { public: void SetFillColor(int fillColor); int GetFillColor()const; virtual ~CFillable(){} private: int m_fillColor; }; 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; };

Slide 70

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

Slide 71

Ромбовидное наследование CAnimal CMammal CWingedAnimal CBat

Slide 72

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

Slide 73

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

Slide 74

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

Slide 75

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

Slide 76

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

Slide 77

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

Slide 78

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

URL: