Наследование и иерархии классов Лекция 06 03 2014

Скачать презентацию Наследование и иерархии классов Лекция 06 03 2014 Скачать презентацию Наследование и иерархии классов Лекция 06 03 2014

Лекция-2.03.ppt

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

Наследование и иерархии классов Лекция 06. 03. 2014 г. 1 Наследование и иерархии классов Лекция 06. 03. 2014 г. 1

В общем случае класс может выполнять две миссии (иметь два вида клиентов): • быть В общем случае класс может выполнять две миссии (иметь два вида клиентов): • быть образцом для создания экземпляров данного класса (объектов); • быть образцом для создания новых классов (подклассов данного класса). Разработчик класса может определить, какие члены класса доступны для его экземпляров, а какие – для подклассов. В языке С++ можно запретить одну из миссий класса – создание экземпляров, если включить в класс чисто виртуальные (pure virtual) функции или сделать все конструкторы класса защищенными, а в языке C# существуют ещё и «запечатанные» (sealed) классы, от которых нельзя наследовать. Лекция 06. 03. 2014 г. 2

Интерфейс для подклассов class D : public B { public: int f() {return f Интерфейс для подклассов class D : public B { public: int f() {return f 1(x); } }; D *pd = new D(); int v = pd -> f(); class B { private: int n; void f(int k) {…} protected: int x; int f 1(int i) {…} public: B() {…} B(int n) {…} int f 2(int m) {…} }; Интерфейс для объектов B b; B *pb = new B(100); int k = pb -> f 2(10); int s = pd -> f 2(5); Лекция 06. 03. 2014 г. 3

Наследование – это такое отношение между классами, при котором один класс повторяет ( «заимствует» Наследование – это такое отношение между классами, при котором один класс повторяет ( «заимствует» ) структуру и поведение другого класса (одиночное наследование) или других классов (множественное наследование). Класс, структура и поведение которого наследуются, называется базовым классом (суперклассом, родительским классом), а класс, «заимствующий» структуру и поведение базового класса – производным классом (подклассом, потомком, дочерним классом). Подкласс может расширять, изменять или ограничивать структуру и поведение своего суперкласса. Некоторые функции-члены базового класса не наследуются автоматически производным классом: конструкторы; деструктор; перегруженная операция присваивания (=). Лекция 06. 03. 2014 г. 4

Наследование является важнейшим механизмом ООП. Ключевая стадия проектирования программ заключается в проектировании иерархий классов, Наследование является важнейшим механизмом ООП. Ключевая стадия проектирования программ заключается в проектировании иерархий классов, представляющих отношение обобщения между базовыми и унаследованными классами программы. Диаграммы иерархий классов изображаются с базовыми классами вверху диаграммы и стрелками, направленными от классов-наследников к непосредственным предкам. Чем ниже класс в иерархии, тем более специализированным он является. Лекция 06. 03. 2014 г. 5

Пример иерархии наследования Shape Ellipse Polygon Circle Rectangle Square Лекция 06. 03. 2014 г. Пример иерархии наследования Shape Ellipse Polygon Circle Rectangle Square Лекция 06. 03. 2014 г. 6

Виды одиночного наследования и правила их синтаксического представления: • открытое • защищенное class D Виды одиночного наследования и правила их синтаксического представления: • открытое • защищенное class D : public B { }; class D : private B { } или class D : B { } ; class D : protected B { }; Лекция 06. 03. 2014 г. 7

Если цепочка наследования описана следующим образом: class B {…}; class D: public B {…}; Если цепочка наследования описана следующим образом: class B {…}; class D: public B {…}; то тем самым компилятору сообщается, что каждый объект типа D является также объектом типа В, но не наоборот. При этом В представляет собой более общую концепцию, чем D, a D - более конкретную концепцию, чем В. Везде, где может быть использован объект В, можно использовать также объект D, потому что D является объектом типа В. С другой стороны, если нужен объект типа D, то объект В не подойдет, поскольку каждый D «является разновидностью» В, но не наоборот. Другими словами: любая функция, которая принимает аргумент типа B (или указатель на B, или ссылку на B), примет объект типа D (или указатель на D, или ссылку на D). Такова интерпретация открытого наследования. Лекция 06. 03. 2014 г. 8

Пример: class B {…}; class D: public B {…}; void f 1(const B&); void Пример: class B {…}; class D: public B {…}; void f 1(const B&); void f 2(const D&); B b; D d; f 1(b); // ok! f 1(d); // ok! f 2(b); // err? f 2(d); // ok! Лекция 06. 03. 2014 г. 9

Открытое наследование реализует отношение «является» , т. е. все, что применимо к базовому классу, Открытое наследование реализует отношение «является» , т. е. все, что применимо к базовому классу, должно быть применимо также и к производному классу. Объект производного класса является также объектом базового класса (поддерживает интерфейс базового класса и, возможно, его расширяет). class B {}; class D : public B {}; public protected private нет доступа Видимость членов класса B в классе B Видимость членов класса B в классе D Лекция 06. 03. 2014 г. 10

Рассмотрим пример, демонстрирующий, что открытое наследование можно применять только тогда, когда отношение между классами Рассмотрим пример, демонстрирующий, что открытое наследование можно применять только тогда, когда отношение между классами действительно соответствует отношению «является» . Квадрат является прямоугольником? class Rectangle { public: virtual void set. Height(int new. Height); virtual void set. Width(int new. Width); virtual int height() const; virtual int width() const; class Square: . . . public Rectangle {. . . }; }; . . . void make. Bigger(Rectangle& r) { Square s; r. set. Width(r. width() + 10); . . . } make. Bigger(s); // ? ? ? Лекция 06. 03. 2014 г. 11

Неприменимость открытого наследования в данном примере объясняется тем, что некоторые утверждения, справедливые для прямоугольника Неприменимость открытого наследования в данном примере объясняется тем, что некоторые утверждения, справедливые для прямоугольника (его ширина может быть изменена независимо от высоты), не выполняются для квадрата (его ширина и высота должны быть одинаковы). В то же время, открытое наследование предполагает, что всё, что применимо к объектам базового класса, также применимо и к объектам производных классов. В ситуации с прямоугольниками и квадратами это условие не выполняется, поэтому использование открытого наследования здесь некорректно. Компилятор, конечно, этого не запрещает, но, не существует гарантий, что такой код будет вести себя должным образом. Если код компилируется, то это еще не значит, что он будет работать. Отношение «является» - не единственное отношение, возможное между классами. Имеются и другие, достаточно распространенные отношения: «содержит» и «реализован посредством» . Они будут рассмотрены позже. Лекция 06. 03. 2014 г. 12

Сокрытие имен при наследовании Поле производного класса скрывает одноименное поле базового класса. Функция-член производного Сокрытие имен при наследовании Поле производного класса скрывает одноименное поле базового класса. Функция-член производного класса скрывает все (!!!) одноименные функции-члены базового класса, за исключением виртуальной функции базового класса с той же сигнатурой, которую она переопределяет. Для доступа к скрытым или переопределенным членам базового класса из класса-наследника или внешней функции используют квалифицированные (полные) имена членов базового класса. Лекция 06. 03. 2014 г. 13

Пример: class B { public: void f 1(){cout<< Пример: class B { public: void f 1(){cout<<"B: : f 1()"<f 1(3); // "D: : f 1(int)" d->f 1(); // ошибка! d->B: : f 1(); // правильно, "B: : f 1()" d->B: : f 1(3); // "B: : f 1(int)" B* b = new D(); b->f 1(3); // работает полиморфизм: "D: : f 1(int)" b->f 1(); // "B: : f 1()" Лекция 06. 03. 2014 г. 14

Следует различать понятия: перегрузка функции (создание новой реализации для данного имени функции); переопределение реализации Следует различать понятия: перегрузка функции (создание новой реализации для данного имени функции); переопределение реализации виртуальной функции, определенной в базовом классе; сокрытие реализации не виртуальной функции, определенной в базовом классе. Они похожи, но обозначают совершенно разные концепции: перегрузка относится к одноименным функциям в одной области видимости; переопределение и сокрытие связаны с наследованием, относятся к одноименным функциям в разных областях видимости (разных классах) и различаются поддержкой полиморфного поведения. Лекция 06. 03. 2014 г. 15

Наследование интерфейса и наследование реализации Варианты наследования функций-членов базового класса: Невиртуальные функции - наследуют Наследование интерфейса и наследование реализации Варианты наследования функций-членов базового класса: Невиртуальные функции - наследуют интерфейс и реализацию функции без возможности переопределения реализации в производном классе. Виртуальные функции - наследуют интерфейс и реализацию функции «по умолчанию» с возможностью переопределения этой реализации (создания новой версии) в производном классе. Чистые виртуальные функции - наследуют только интерфейс функции; реализация функции «по умолчанию» отсутствует и должна быть выполнена в производном классе. Лекция 06. 03. 2014 г. 16

Рассмотрим фрагмент иерархии классов для представления геометрических фигур в графическом приложении: class Shape { Рассмотрим фрагмент иерархии классов для представления геометрических фигур в графическом приложении: class Shape { public: virtual void draw() const = 0; virtual void error (const std: : string& msg); int object. ID() const; }; class Polygon: public Shape {. . . }; class Ellipse: public Shape {. . . }; Shape - это абстрактный класс, поэтому создавать объекты класса Shape нельзя. Несмотря на это, Shape оказывает сильное влияние на все открыто наследующие ему классы, т. к. интерфейс его функций-членов наследуется всегда. В классе Shape объявлены три функции: draw - выводит текущий объект на дисплей; error - вызывается функциями-членами, если необходимо сообщить об ошибке и object. ID - возвращает уникальный целочисленный идентификатор текущего объекта. Каждая из трех функций объявлена по-разному: draw - чисто виртуальная; error - просто виртуальная; object. ID - невиртуальная функция. Каковы практические последствия этих различий? Лекция 06. 03. 2014 г. 17

Чисто виртуальные функции должны быть заново объявлены в любом конкретном наследующем их классе, в Чисто виртуальные функции должны быть заново объявлены в любом конкретном наследующем их классе, в абстрактном классе они обычно не определяются. Следовательно, цель объявления чисто виртуальной функции состоит в том, чтобы производные классы наследовали только ее интерфейс. Ситуация с обычными виртуальными функциями иная – они обеспечивают реализацию, которую подклассы могут переопределить. Следовательно, цель их объявления - наследовать в производных классах как интерфейс, так и ее реализацию по умолчанию. Для не виртуальной функции не предполагается, что она будет вести себя иначе в производных классах. В действительности не виртуальные функции-члены выражают инвариант относительно специализации, т. к. определяют поведение, которое должно сохраняться независимо от того, как специализируются производные классы. Следовательно, цель объявления не виртуальной функции - заставить производные классы наследовать как её интерфейс, так и обязательную реализацию. Лекция 06. 03. 2014 г. 18

Закрытое и защищенное наследование Закрытое наследование не реализует отношение «является» и позволяет скрыть интерфейс Закрытое и защищенное наследование Закрытое наследование не реализует отношение «является» и позволяет скрыть интерфейс базового класса (или часть интерфейса); объекты производного класса не приводятся автоматически к объектам базового класса. При закрытом наследовании наследуется только реализация, без интерфейса; это - отношение «реализовано посредством» . Закрытое наследование похоже на агрегацию. class B {}; class D : private B {}; public protected private нет доступа Видимость членов класса B в классе B Видимость членов класса B в классе D Лекция 06. 03. 2014 г. 19

Пример закрытого наследования Реализация класса Stack на базе класса Vector. //Stack. h - объявление Пример закрытого наследования Реализация класса Stack на базе класса Vector. //Stack. h - объявление класса Stack #include #include "Vector. h" using namespace std; class Stack : Vector { public: using Vector : : operator<<; // «Втолкнуть» в стек Stack& operator>>(double&); // «Вытолкнуть из стека friend ostream& operator<<(ostream&, Stack&); // Вывод в поток }; //Stack. cpp – реализация класса Stack double DUM = 1 e 10; Stack& Stack: : operator>>(double& x) { if(!(*this) == 0) x = DUM; else { int n = !(*this)-1; x = (*this)[n]; (*this) ^ n; } return *this; } ostream& operator<<(ostream& s, Stack& v) { if(!v == 0) s << "Stack is empty. n"; else { s << "Stack = ["; for(int i = 0; i < !v; ++i) { s << v[i]; if(i == !v-1) s << "]n"; else s << ", "; } } return s; Лекция 06. 03. 2014 г. } 20

Пример закрытого наследования Тестирование класса Stack. //Ex 060. cpp #include <cstdlib> #include <iostream> #include Пример закрытого наследования Тестирование класса Stack. //Ex 060. cpp #include #include #include "Stack. h" using namespace std; int main() { Stack s; s << 10 << 20; cout << "s : " << s; double x, y, z; s >> x >> y >> z; cout << "x = " << x << endl; cout << "y = " << y << endl; cout << "z = " << z << endl; cout << "s : " << s; s << 40 << 50 << 60; cout << "s : " << s; system("PAUSE"); return 0; } Лекция 06. 03. 2014 г. 21

Защищенное наследование class B {}; class D : protected B {}; public protected private Защищенное наследование class B {}; class D : protected B {}; public protected private нет доступа Видимость членов класса B в классе B Видимость членов класса B в классе D Лекция 06. 03. 2014 г. 22

Порядок вызова конструкторов и деструкторов при наследовании Порядок вызова конструкторов при наследовании: 1. Сначала Порядок вызова конструкторов и деструкторов при наследовании Порядок вызова конструкторов при наследовании: 1. Сначала вызываются конструкторы базовых классов в порядке их перечисления в списке наследования. 2. Затем вызываются конструкторы полей класса в порядке их объявления в классе. 3. После конструирования всех базовых классов и полей класса выполняется тело конструктора класса. Приведенный порядок применяется рекурсивно и не зависит от порядка, указанного в списках инициализации конструкторов класса. Лекция 06. 03. 2014 г. 23

Порядок вызова деструкторов при наследовании: Деструкторы гарантированно вызываются в порядке, обратном порядку вызова конструкторов: Порядок вызова деструкторов при наследовании: Деструкторы гарантированно вызываются в порядке, обратном порядку вызова конструкторов: 1. Сначала вызывается тело деструктора удаляемого объекта. 2. Затем деструкторы полей класса в порядке, обратном порядку их объявления в классе. 3. В завершение вызываются деструкторы базовых классов, начиная с последнего в списке наследования. Данный порядок также применяется рекурсивно. Лекция 06. 03. 2014 г. 24

Пример вызова конструкторов и деструкторов class B { public: B(){cout << Пример вызова конструкторов и деструкторов class B { public: B(){cout << "B()"<

Пример сложной иерархии классов class A { } class B : public A { Пример сложной иерархии классов class A { } class B : public A { } class C : public A { } class E : public B, public C, public D { } class D : public A { } class F : public B, public C, public D { } class G : public E, public F { }. . . G g; . . . A B A C A D E A B A C A D F G ~G ~F ~D ~A ~C ~A ~B ~A ~E ~D ~A ~C ~A ~B ~A Лекция 06. 03. 2014 г. 26

Виртуальное наследование class A { } class B : virtual public A { } Виртуальное наследование class A { } class B : virtual public A { } class C : virtual public A { } class E : virtual public B, virtual public C, virtual public D { } class D : virtual public A { } class F : virtual public B, virtual public C, virtual public D { } class G : public E, public F { }. . . G g; . . . A B C D E F G ~G ~F ~E ~D ~C ~B ~A Лекция 06. 03. 2014 г. 27

Позднее связывание и виртуальные функции Связывание – это сопоставление вызова функции с ее телом Позднее связывание и виртуальные функции Связывание – это сопоставление вызова функции с ее телом (реализацией). Существует два вида связывания: • раннее – выполняется компилятором и компоновщиком до запуска программы; • позднее (динамическое) – производится во время выполнения программы в зависимости от фактического типа объекта. В C++ механизм позднего связывания работает только для виртуальных функций и только при использовании для вызова функции указателей на объекты базового класса, содержащего виртуальные функции (т. е. для полиморфного вызова). Если функция объявляется виртуальной (virtual) в базовом классе, то она становится виртуальной во всех производных от него классах. Лекция 06. 03. 2014 г. 28

Невиртуальная функция (раннее связывание) class B { public: void print() {cout << Невиртуальная функция (раннее связывание) class B { public: void print() {cout << "B: : print()" << endl; } }; class D: public B { public: void print() {cout << "D: : print()" << endl; } }; class E: public D { public: void print() {cout << "E: : print()" << endl; } }; void Print(B *z) {z->print(); } // всегда вызывается B: : print(). . . D *v = new D; // создается объект класса D Print(v); // B: : print() B *x = new B; // создается объект класса B Print(x); // B: : print() x = new D; // создается объект класса D Print(x); // B: : print() x = new E; // создается объект класса E Print(x); // B: : print() Всегда вызывается функция базового класса. Лекция 06. 03. 2014 г. 29

Виртуальная функция (позднее связывание) class B { public: virtual void print() {cout << Виртуальная функция (позднее связывание) class B { public: virtual void print() {cout << "B: : print()" << endl; } }; class D: public B { public: void print() {cout << "D: : print()" << endl; } }; class E: public D { public: void print() {cout << "E: : print()" << endl; } }; void Print(B *z) {z->print(); }. . . D *v=new D; // создается объект класса D Print(v); // D: : print() B *x=new B; // создается объект класса B Print(x); // B: : print() x = new D; // создается объект класса D Print(x); // D: : print() x = new E; // создается объект класса E Print(x); // E: : print() Тип вызываемого конструктора определяет версию реализации, переопределяющую базовую версию виртуальной функции. Лекция 06. 03. 2014 г. 30

D 2: : D 1: : x B: : void print() {cout << “D D 2: : D 1: : x B: : void print() {cout << “D 2: : print()" << endl; } void print() {cout << “D 1: : print()" << endl; } virtual void print() {cout << "B: : print()" << endl; } переопределение Виртуальные функции B *x = new D 2; Тип вызываемого конструктора определяет версию реализации, переопределяющую базовую версию виртуальной функции. Лекция 06. 03. 2014 г. 31

Еще один пример, показывающий, что тип вызываемого конструктора определяет версию реализации, переопределяющую базовую версию Еще один пример, показывающий, что тип вызываемого конструктора определяет версию реализации, переопределяющую базовую версию виртуальной функции. class B { public: virtual void f(){cout<<"B: : f()"<f(); // D: : f() e->f(); // D: : f() Лекция 06. 03. 2014 г. 32

Пример с виртуальными функциями class B { public: virtual void F 1() {cout<< Пример с виртуальными функциями class B { public: virtual void F 1() {cout<<"B: : F 1()"<

Механизм вызова виртуальных функций Объект D 1 Массив указателей типа B* z[0]=new D 1 Механизм вызова виртуальных функций Объект D 1 Массив указателей типа B* z[0]=new D 1 VPTR z[3]=new E 2 &D 1: : F 1 &D 1: : F 2 &D 1: : F 3 Объект D 2 &D 2: : F 1 VPTR &D 2: : F 2 Объект E 1 &E 1: : F 1 z[1]=new D 2 z[2]=new E 1 Таблицы виртуальных функций VPTR Объект E 2 VPTR Лекция 06. 03. 2014 г. &D 2: : F 3 &E 1: : F 2 &D 1: : F 3 &D 2: : F 1 &E 2: : F 2 &D 2: : F 3 34

Механизм вызова виртуальных функций В одной цепочке наследования для каждого не абстрактного класса создается Механизм вызова виртуальных функций В одной цепочке наследования для каждого не абстрактного класса создается полная таблица виртуальных функций (VTBL), содержащая адреса всех виртуальных функций. Если функция в классе не переопределялась, в соответствующую ячейку таблицы заносится адрес функции ближайшего класса-предка. Независимо от того, к какому из производных классов принадлежит объект, адреса его виртуальных функций следуют в VTBL в одинаковом порядке, поэтому они вызываются одинаковым способом – простым числовым смещением внутри VTBL. Расширяемость и виртуальные функции В хорошо спроектированной объектно-ориентированной программе функции должны взаимодействовать только с интерфейсом базового класса. Такие программы хорошо расширяются – новые возможности добавляются путем создания новых типов данных посредством их наследования от базового класса, содержащего виртуальные функции. Лекция 06. 03. 2014 г. 35

Чисто виртуальные (pure virtual) функции. Чисто виртуальные функции - это функции, в объявлении которых Чисто виртуальные (pure virtual) функции. Чисто виртуальные функции - это функции, в объявлении которых присутствует инициализатор 0. Класс, содержащий хотя бы одну такую функцию, называется абстрактным. Невозможно создать объект абстрактного класса. class B { public: virtual void print() = 0; }; . . . B b; // ошибка компиляции! Смысл чисто виртуальной функции состоит в необходимости ее переопределения в производных классах. Чисто виртуальные функции позволяют сформировать «костяк» иерархии, т. е. необходимый минимальный (базовый) интерфейс, определяющий общие черты всех производных классов. Часто базовые классы состоят только из чисто виртуальных функций. Лекция 06. 03. 2014 г. 36

Чисто виртуальные (pure virtual) функции. Чисто виртуальная функция может содержать реализацию (тело); в этом Чисто виртуальные (pure virtual) функции. Чисто виртуальная функция может содержать реализацию (тело); в этом случае говорят, что у чисто виртуальной функции определено поведение по умолчанию. class B { public: virtual int f() const = 0; }; int B: : f() const { return 100; } class D : public B { public: int f() const { return B: : f(); } }; B *b = new D; cout << b->f() << endl; // 100 Лекция 06. 03. 2014 г. 37

Особенности деструкторов полиморфных классов. Виртуальные деструкторы В приведенном ниже коде удаление объекта типа D Особенности деструкторов полиморфных классов. Виртуальные деструкторы В приведенном ниже коде удаление объекта типа D по указателю z[1] выполнено некорректно, в результате может произойти «утечка памяти» . Причина – не виртуальный деструктор в полиморфном классе. class B { public: virtual void f() {} ~B() {cout << "~B()" <

В этом же примере удаление объекта типа D по указателю z[1] корректно, потому что В этом же примере удаление объекта типа D по указателю z[1] корректно, потому что в полиморфном классе объявлен виртуальный деструктор. class B { public: virtual void f() {} virtual ~B() {cout << "~B()" << endl; } }; class D : public B { public: void f() {} ~D() {cout<< "~D()" << endl; } }; B *z[] = {new B, new D}; delete z[0]; // ~B() delete z[1]; // ~D() ~B() Лекция 06. 03. 2014 г. 39

Деструктор класса следует объявлять виртуальным тогда и только тогда, когда этот класс содержит хотя Деструктор класса следует объявлять виртуальным тогда и только тогда, когда этот класс содержит хотя бы одну виртуальную функцию. Можно объявить деструктор класса чисто виртуальным, если класс нужно сделать абстрактным (запретить создание объектов этого класса). Такой деструктор обязательно должен иметь реализацию по умолчанию. class B { public: virtual ~B()=0; // чисто виртуальный деструктор }; B: : ~B() {cout<<"~B()"<