Что такое композиция? Композиция (агрегирование, включение) – простейший механизм для создания нового класса путем объединения нескольких объектов существующих.

Презентация:



Advertisements
Похожие презентации
Наследование Полиморфизм ВЫЗОВ КОНСТРУКТОРОВ И ДЕСТРУКТОРОВ ПРИ НАСЛЕДОВАНИИ.
Advertisements

Преобразования типов В языке C/C++ имеется несколько операций преобразования типов. Они используются в случае, если переменная одного типа должна рассматриваться.
НГТУ, каф. ВТ Наследование в С++ Макаревич Л. Г.НГТУ, каф. ВТ Наследование в С++ Макаревич Л. Г.
Основы информатики Классы Заикин Олег Сергеевич zaikin.all24.org
Прикладное программирование кафедра прикладной и компьютерной оптики Наследование.
Наследование Наследование – это отношение является между классами. class Person { string first_name; int birth_year;... } class Student : Person { float.
НаследованиеНаследование2 class Point { double x; double y; Color color; }; class Radius { Point center; double radius; };
Полиморфизм. Полиморфизм – это свойство системы использовать объекты с одинаковым интерфейсом без информации о типе и внутренней структуре объекта.
Дружественные функции Дружественные функции – это функции, объявленные вне класса, но имеющие доступ к закрытым и защищенным полям данного класса Дружественная.
Практическое занятие 6. Функции. Большинство языков программирования используют понятия функции и процедуры. C++ формально не поддерживает понятие процедуры,
Лекция 4. Введение в С++ Наследование, множественное наследование. Конструкторы, деструкторы. Виртуальные функции.
Объектно-ориентированное программирование С++. Лекция 6 Карпов В.Э.
Обработка исключительных ситуаций Исключительная ситуация (исключение) – это ошибка, возникающая во время выполнения программы. Например, ошибка работы.
Что такое обобщенное программирование? Парадигма программирования, заключающаяся в написании алгоритмов, которые можно применять к различным типам данных.
Наследование и полиморфизм. «Быть» или «Иметь» а так же «Точно» или «Как получится»
Множественное наследование class A {... }; class B {... }; class C : public A, protected B {... }; !!! Спецификатор доступа распространяется только на.
Производные классы Определение класса посредством добавления возможностей к уже имеющемуся классу без перепрограммирования или перекомпиляции самого класса.
Лекция 10. Введение в ООП. Часть 3 Красс Александр СПбГУ ИТМО, 2008.
Интерфейсы Лекция 4. Реализуйте очередь в виде списка, содержащую комплексные числа Реализуйте методы void Enqueue(Complex с ) – помещает число в очередь.
Полиморфизм Полиморфизм (polymorphism) - последний из трех "китов", на которых держится объектно-ориентированное программирование Слово это можно перевести.
Транксрипт:

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

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

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

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

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

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

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

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

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

Пример – иерархия в человеческом обществе 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

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

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

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

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

Пример – стек целых чисел 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; }; Нельзя использовать открытое наследование Стек не является массивом, но пользуется реализацией массива К стеку не применимы операции индексированного доступа

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

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

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

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

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

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

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

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

Пример class CEmployee { public: std::string GetName()const { return m_name; } protected: CEmployee(std::string const& name) :m_name(name) { std::cout

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

Пример class CTable { public: CTable(std::string const& dbFileName) { m_tableFile.Open(dbFileName); std::cout

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

Пример class CBase { public: void Print() { std::cout

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

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 * m_radius * m_radius;} private: double m_radius; };

Так, вроде, все работает: int main(int argc, char * argv[]) { CCircle circle(10); CRectangle rectangle(20, 10); std::cout

А вот так - нет void PrintShapeArea(CShape const& shape) { std::cout

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

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

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 * m_radius * m_radius;} private: double m_radius; };

Теперь заработало как надо void PrintShapeArea(CShape const& shape) { std::cout

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

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

Проблемы при использовании невиртуального деструктора class CBase { public: CBase():m_pBaseData(new char [100]) { std::cout

Исправляем проблему, объявив деструктор виртуальным class CBase { public: CBase():m_pBaseData(new char [100]) { std::cout

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

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

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

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

Пример 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 {... }

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

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

Пример 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 (pAnimal) != NULL) std::cout

Пример 2 – приведение ссылок CMammal const& MakeMammal(CAnimal const & animal) { return dynamic_cast (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

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

Решение без 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

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

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

Пример // интерфейс объектов, которые можно нарисовать 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; };

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

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

Пример проблемы ромбовидного наследования // Животное 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; }

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

Пример использования виртуального наследования // Животное 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; }

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

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

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

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