Скачать презентацию Программирование на С Тема 2 1 Отношения между Скачать презентацию Программирование на С Тема 2 1 Отношения между

Качесова Л.Ю.-Прогр.2--20130226093349.ppt

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

Программирование на С++ Тема 2. 1 Отношения между классами 16. 02. 2018 1 Программирование на С++ Тема 2. 1 Отношения между классами 16. 02. 2018 1

Отношения между классами • Является (is a) – Открытое (public - ) наследование; • Отношения между классами • Является (is a) – Открытое (public - ) наследование; • Содержит (has a) – Наследования нет! В качестве члена данных класс содержит объект(ы) другого класса; • Подобен (as a) – Закрытое (private или protected) наследование. 16. 02. 2018 2

Ø Открытое наследование поддерживает между классами (и соответственно между экземплярами этих классов т. е. Ø Открытое наследование поддерживает между классами (и соответственно между экземплярами этих классов т. е. объектами) отношение «is-a» ( «является разновидностью» ). Базовый класс описывает общие поля данных и реализует только самые общие методы, описывающие поведение объектов его иерархии. Производные классы всегда является разновидностью базового, т. е. каждый производный класс использует для описания структуры и поведения объектов поля данных и методы базового класса и добавляет свои специфические поля данных и методы. Ø Язык С++ допускает как одиночное (или простое), так и множественное наследование. Ø Наследование называют простым, когда каждый класс-потомок имеет только один родительский класс. Наследование называют множественным, если производный класс наследует свойства двух и более «родителей» . Ø На рисунке следующего слайда представлена иерархия классов при открытом одиночном (простом)наследовании (три уровня иерархии). Ø Ниже на рисунке показано сколько будет зарезервировано памяти под объекты каждого класса. 16. 02. 2018 3

Открытое одиночное (простое) наследование Базовый класс А Поле а Методы класса А Уровень 0 Открытое одиночное (простое) наследование Базовый класс А Поле а Методы класса А Уровень 0 Уровень 1 Производный класс C Поле c Методы класса C Производный класс В Поле b Методы класса B Уровень 2 Производный класс D Поле d Методы класса D A Поле а 16. 02. 2018 B Поле а Поле b Производный класс E Поле e Методы класса E C Поле а Поле c D Поле а Поле b Поле d Производный класс F Поле f Методы класса F E Поле а Поле b Поле e F Поле а Поле c Поле f 4

Открытое одиночное (простое) наследование • Синтаксис объявления производного класса: class Имя_производного_класса : спецификатор_наследования Имя_базового_класса Открытое одиночное (простое) наследование • Синтаксис объявления производного класса: class Имя_производного_класса : спецификатор_наследования Имя_базового_класса { поля данных производного класса, методы производного класса }; • Пример: class B { список членов класса B} // базовый класс class P : public B // производный класс { список членов класса P} Спецификатор доступа Доступ посредством Доступ из методов в базовом классе объекта произв. класса к элементам базового кл. public: доступен protected: не доступен private: не доступен 16. 02. 2018 5

Ø Спецификаторы наследования: Ø public – открытое наследование; Ø private, protected – закрытое (защищенное) Ø Спецификаторы наследования: Ø public – открытое наследование; Ø private, protected – закрытое (защищенное) наследование. Закрытое наследование рассмотрим позже. Ø Если спецификатор наследования не указан, то компилятор по умолчанию считает, что он private! Ø В примере на слайде показано открытое одиночное наследование, производный класс P является разновидностью класса B. Такое объявление говорит компилятору о том, что: Ø Класс P включает в себя, как составляющую весь класс В; Ø В зависимости от спецификатора наследования и спецификаторов доступа методы класса P имеют право обращаться к членам класса В или нет (таблица на слайде). Ø Замечание. Конструкторы, деструкторы и оператор присваивания не наследуются! Наследуются данные и остальные методы класса. 16. 02. 2018 6

Открытое одиночное (простое) наследование • • Пример: порядок вызовов конструкторов и деструкторов при создании Открытое одиночное (простое) наследование • • Пример: порядок вызовов конструкторов и деструкторов при создании объекта производного класса class A{. . . }; class B : public A{. . . }; class C : public B{. . . }; A: : A() -> B: : B() -> C: : C() С: : ~С() -> B: : ~B() -> A: : ~A() Синтаксис объявления конструктора производного класса: Имя конструктора_произв_класса(общий_список_параметров ) : имя_конструктора_базового_класса (список_параметров) { // Тело конструктора производного класса } ; 16. 02. 2018 7

Ø Рассмотрим порядок вызовов конструкторов при создании объекта производного класса. Создание объекта производного класса Ø Рассмотрим порядок вызовов конструкторов при создании объекта производного класса. Создание объекта производного класса начинается с унаследованной базовой части, поэтому порядок вызовов конструкторов в С++ фиксирован: сначала вызывается конструктор базового класса, затем вызываются конструкторы производный классов согласно иерархии наследования. Ø Осуществляется вызов конструктора базового класса в том коде, который компилятор генерирует по открывающей фигурной скобке конструктора производного класса. Ø Деструкторы при разрушении объекта производного класса вызываются в порядке, обратном вызову конструкторов, сначала разрушаются специализированные части, затем общие. Осуществляется вызов деструкторов базового класса в том коде, который компилятор генерирует по закрывающейся фигурной скобке деструктора производного класса. Ø Так конструктор базового класса выполняется раньше, чем конструктор производного, то конструктору базового класса нужно явно передать предназначенные ему параметры. Поэтому для передачи параметров конструктору базового класса используется специальная запись (см. на слайде синтаксис определения конструктора производного класса). Ø В примере 1 на следующих слайдах показано создание и использование объектов производного класса Dog. Ø Если вы явно не определяете в производном классе конструктор копирования, то автоматический конструктор копирования (сгенерированный компилятором) может вызвать конструктор копирования базового класса (определенный программистом или же автоматический). Но, если Вы определяете в производном классе конструктор копирования, но не вызываете явно конструктор копирования базового класса, компилятор вызывать сам конструктор копирования базового класса не будет, а базовая часть нового объекта будет сконструирована с помощью конструктора по умосланию. Ø В примере 1 на следующих слайдах так же показан вызов конструктора копирования базового класса в конструкторе копирования производного класса. 16. 02. 2018 8

Открытое одиночное (простое) наследование • Пример 1: Создание и использование объектов произв. класса // Открытое одиночное (простое) наследование • Пример 1: Создание и использование объектов произв. класса // Mammal. h #ifndef MAMMAL_H_ #define MAMMAL_H_ class Mammal // объявление класса млекопитающее { protected: int age; // возраст char* name; // кличка public: Mammal(); Mammal(char* Name, int Age); ~Mammal(); Mammal(const Mammal &m); int get. Age(); //возвращает возраст void set. Age(int Age); //устанавливает возраст void Speak()const; // голос }; #endif 16. 02. 2018 9

Открытое одиночное (простое) наследование • Пример: Создание и использование объектов произв. класса // Mammal. Открытое одиночное (простое) наследование • Пример: Создание и использование объектов произв. класса // Mammal. cpp #include "Mammal. h" #include #include using namespace std; // конструктор по умолчанию Mammal: : Mammal() { name = new char [256]; //выделяем память strcpy(name, ""); //копируем имя age = 1; cout << "Mammal constructor!n"; } // деструктор Mammal: : ~Mammal() { delete []name; cout << "Mammal destructor!n"; } 16. 02. 2018 10

Открытое одиночное (простое) наследование • Пример: Создание и использование объектов произв. класса // конструктор Открытое одиночное (простое) наследование • Пример: Создание и использование объектов произв. класса // конструктор с параметрами Mammal: : Mammal( char* Name, int Age) { int leng = strlen(Name); //выч. длины имени name = new char [leng+1]; //выделяем память strcpy(name, Name); //копируем имя age = Age; cout << "Mammal (char* int) constructor!n"; } // конструктор копирования Mammal: : Mammal(const Mammal &m) { int leng = strlen(m. name); //выч-е длины имени name = new char[leng+1]; //выд-е памяти для копии strcpy(name, m. name); //копируем имя age = m. age; //копируем возраст } 16. 02. 2018 11

Открытое одиночное (простое) наследование • Пример: Создание и использование объектов произв. класса // методы Открытое одиночное (простое) наследование • Пример: Создание и использование объектов произв. класса // методы доступа int Mammal: : get. Age(){ return age; } void Mammal: : set. Age(int Age){ age = Age; } // голос void Mammal: : Speak()const { cout << " Mammal " << name ; cout << " is " << age << " old years sound!nn "; } 16. 02. 2018 12

Открытое одиночное (простое) наследование • Пример: Создание и использование объектов произв. класса // Dog. Открытое одиночное (простое) наследование • Пример: Создание и использование объектов произв. класса // Dog. h #ifndef DOG_H_ #define DOG_H_ #include "Mammal. h" enum BREED {YORKIE, SHETLAND, DOBERMAN}; class Dog : public Mammal { private: char * hname; // имя хозяина BREED breed; // порода public: Dog(); ~Dog(); Dog(const Dog &d); Dog(char* Name, int Age, char* h. Name, BREED Breed); int get. Breed()const; void set. Breed(BREED Breed); void Wag. Tail(); }; #endif 16. 02. 2018 13

Открытое одиночное (простое) наследование • Пример: Создание и использование объектов произв. класса // Dog. Открытое одиночное (простое) наследование • Пример: Создание и использование объектов произв. класса // Dog. cpp #include "Dog. h" #include #include using namespace std; Dog: : Dog(): Mammal() { hname = new char [256]; //выделяем память strcpy(hname, ""); //копируем имя breed = YORKIE; cout << "Dog constructor!n"; } Dog: : ~Dog() { delete []name; cout << "Dog destructor!n"; } 16. 02. 2018 14

Открытое одиночное (простое) наследование • Пример: Создание и использование объектов произв. класса // Dog. Открытое одиночное (простое) наследование • Пример: Создание и использование объектов произв. класса // Dog. cpp Dog: : Dog(char* Name, int Age, char* h. Name, BREED Breed): Mammal(Name, Age) { int leng=strlen(h. Name); //выч. длины имени hname = new char [leng+1]; //выделяем память strcpy(hname, h. Name); //копируем имя breed = Breed; cout << "Dog (char* int char* BREED) constructor!n"; } int Dog: : get. Breed()const{return breed; } void Dog: : set. Breed(BREED Breed){breed = Breed; } void Dog: : Wag. Tail() { cout << " Dog " << name ; cout << " is " << age << " old years tail wagging. . n "; cout << name << " is dog " << hname << "n"; } 16. 02. 2018 15

Открытое одиночное (простое) наследование • Пример: Создание и использование объектов произв. класса // Dog. Открытое одиночное (простое) наследование • Пример: Создание и использование объектов произв. класса // Dog. cpp Dog: : Dog(const Dog &d): Mammal(d) // вызов конструктора копирования // базового класса { int leng = strlen(d. hname); //выч-е длины имени hname = new char[leng+1]; //выд-е памяти для копии strcpy(hname, d. hname); //копируем имя breed = d. breed; //копируем породу } 16. 02. 2018 16

Открытое одиночное (простое) наследование • Пример: Создание и использование объектов произв. класса // main. Открытое одиночное (простое) наследование • Пример: Создание и использование объектов произв. класса // main. cpp #include "Dog. h" #include #include using namespace std; int main() { Dog 1; Dog 1. Speak(); Dog 2("Reks", 6, "Petr", DOBERMAN); Dog 3 = Dog 2; Dog 3. Speak(); Dog 3. Wag. Tail(); } 16. 02. 2018 17

16. 02. 2018 18 16. 02. 2018 18

Полиморфизм • Полиморфизм в русском языке: переводить (книгу, бабушку через дорогу, деньги. . . Полиморфизм • Полиморфизм в русском языке: переводить (книгу, бабушку через дорогу, деньги. . . ); • Глагол “переводить” можно связать с конкретным набором действий только тогда, когда известен сам объект воздействия. • Полиморфизм в С++ - это свойство одного и того же кода С++ вести себя по-разному в зависимости от текущих условий выполнения программы. То есть во время выполнения программы в том месте, где программист вызывает метод класса, компилятор закладывает возможность вызова методов разных классов, связанных между собой наследованием. 16. 02. 2018 19

Ø Полиморфизм в С++ реализуется с использованием механизма позднего связывания, виртуальных функций и указателя Ø Полиморфизм в С++ реализуется с использованием механизма позднего связывания, виртуальных функций и указателя (указателей) на базовый класс. Ø С++ - язык программирования, который совмещает, как процедурные, так и объектноориентированные возможности. Поэтому С++ использует и раннее, и позднее связывание и предоставляет программисту преимущества (и недостатки!) и того, и другого. Ø Замечание: следует понимать, что при использовании раннего связывания программисту приходится реализовывать сложную логику работы программы, например, используя громоздкие конструкции типа switch-case, в то время как при позднем связывании платой за облегчение жизни программиста является “более тяжелый” исполняемый код, занимающий больше памяти и/или дольше работающий. Ø Раннее связывание – это просто перегрузка имен функций. Этот механизм работает на этапе компиляции, то есть при наличии нескольких функций с одинаковым именем компилятор генерирует вызов конкретного тела функции. В случае перегрузки имен глобальных функций компилятор определяет, которую из функций следует вызвать в зависимости от количества и/или типа параметров. Это также справедливо при перегрузке имен методов одного и того же класса. Но при наследовании добавляется еще одна возможность – перегрузка методов, которые выглядят абсолютно одинаково (но действия совершают разные). В этом случае метод производного класса замещает для компилятора одноименный метод базового класса. Ø Для того, чтобы продемонстрировать удобство использования полиморфизма, реализуем одну и ту же задачу с помощью раннего и позднего связывания. Ø На следующих слайдах показано : - использование раннего связывания (пример 1); - перегрузка имен функций при наследовании – метод производного класса замещает одноименный метод базового класса (пример 2). 16. 02. 2018 20

 • Пример 1: Раннее связывание #include <string. h> #include <iostream> using namespace std; • Пример 1: Раннее связывание #include #include using namespace std; enum ANIMAL_TYPE{ANI, DOG, CAT}; class Animal // базовый класс, описывающий животное { public: void Voice(){cout << "n? ? ? "; }; }; class Dog : public Animal { public: void Voice(){cout << "n. Гав!"; }; }; class Cat : public Animal { public: void Voice(){cout << "n. Мяу!"; }; }; 16. 02. 2018 21

 • Пример 1: Раннее связывание void F 1( Animal* p) { p -> • Пример 1: Раннее связывание void F 1( Animal* p) { p -> Voice(); } void F 2( Animal* p, ANIMAL_TYPE type) { switch (type) { case DOG: { Dog* pd = static_cast(p); pd -> Voice(); break; } case CAT: { Cat* pc = static_cast(p); pc -> Voice(); break; } default: p->Voice(); } } 16. 02. 2018 22

 • Пример 1: Раннее связывание int main() { Animal a; Dog d; Cat • Пример 1: Раннее связывание int main() { Animal a; Dog d; Cat c; F 1 (&a); // "? ? ? " F 1 (&d); // "? ? ? " F 1 (&c); // "? ? ? " F 2 (&a, ANI); // "? ? ? " F 2 (&d, DOG); // "Гав!" F 2 (&c, CAT); // "Мяу!" } 16. 02. 2018 23

Ø F 1 - глобальная функция, которая имеет формальный параметр - указатель на базовый Ø F 1 - глобальная функция, которая имеет формальный параметр - указатель на базовый класс Animal, а сам объект, адрес которого получает функция, может быть как базового типа Animal, так и любого производного типа – Dog или Cat. В функции F 1()во всех случаях (не имеет значения, какого типа объект!) будет вызван метод Voice() базового класса Animal. Ø F 2 - глобальная функция, которая имеет формальный параметр - указатель на базовый класс Animal, а сам объект, адрес которого получает функция, может быть как базового типа Animal, так и любого производного типа – Dog или Cat -> так как нам нужна разная функциональность для объектов разных производный классов, а в нашем распоряжении только указатель на базовый класс Animal, приходиться вводить дополнительный параметр type: признак с каким именно объектом мы имеем дело (признак – какое животное). 16. 02. 2018 24

 • Пример 2: Перегрузка метода с его замещением class A { public: void • Пример 2: Перегрузка метода с его замещением class A { public: void f(int); }; class B: public A { public: void f(char*); }; int main() { B b; b. f(3); //ошибка! Посредством объекта производного // класса компилятор «не видит» одноименный метод // базового класса b. A: : f(3); //!!! } 16. 02. 2018 25

 • Пример 3: Позднее связывание. Виртуальные функции #include <string. h> #include <iostream> using • Пример 3: Позднее связывание. Виртуальные функции #include #include using namespace std; class Animal // базовый класс, описывающий животное { public: virtual void Voice(){cout << "n? ? ? "; }; }; class Dog : public Animal { public: virtual void Voice(){cout << "n. Гав!"; }; }; class Cat : public Animal { public: virtual void Voice(){cout << "n. Мяу!"; }; }; 16. 02. 2018 26

 • Пример 3: Позднее связывание. Виртуальные функции void F( Animal* p) { p • Пример 3: Позднее связывание. Виртуальные функции void F( Animal* p) { p -> Voice(); } int main() { Animal a; Dog d; Cat c; F(&a); // "? ? ? " F(&d); // "Гав!" F(&c); // "Мяу!" } 16. 02. 2018 27

Ø Позднее связывание имеет смысл только для классов, связанных наследованием. Для того чтобы метод Ø Позднее связывание имеет смысл только для классов, связанных наследованием. Для того чтобы метод класса С++ стал полиморфным, при его объявлении нужно указать ключевое слово virtual. Если реализация метода отделена от его объявления, то при определении метода ключевое слово virtual не должно фигурировать, иначе компилятор выдаст ошибку. Ø Полиморфные методы должны не только называться одинаково, но у них также должны совпадать количество и типы параметров, а также тип возвращаемого значения, а вот реализации у таких методов разные. Позднее связывание – это механизм этапа выполнения, то есть, какая из функций будет вызвана, определяется только во время выполнения программы. 16. 02. 2018 28

 • Пример 4: Механизм вызова виртуальной функции class A { int m_a; public: • Пример 4: Механизм вызова виртуальной функции class A { int m_a; public: virtual void }; class B: public A { int m_b; public: virtual void }; 16. 02. 2018 A: : vtab vf 1(){ … }; vf 2(){ … }; &(A: : vf 1()) &(A: : vf 2()) B: : vtab vf 1(){ … }; vf 2(){ … }; &(B: : vf 1()) &(B: : vf 2()) 29

 • Пример 4: Механизм вызова виртуальной функции int main() { A a(1); B • Пример 4: Механизм вызова виртуальной функции int main() { A a(1); B b (2, 3); B b 1 (4, 5); gf 1(&a); gf 1(&b); gf 2(a); gf 2(b); a. vf 1(); //!!! } void gf 1(A* p) { p->vf 1(); } void gf 2(A& r) { r. vf 1(); } 16. 02. 2018 Объект a A: : vtab vptr A: : m_a &(A: : vf 1()) &(A: : vf 2()) Объект b B: : vtab vptr A: : m_a B: : m_b &(B: : vf 1()) &(B: : vf 2()) Объект b 1 vptr A: : m_a B: : m_b 30

Ø Позднее связывание имеет смысл только для классов, связанных наследованием. Для того чтобы метод Ø Позднее связывание имеет смысл только для классов, связанных наследованием. Для того чтобы метод класса С++ стал полиморфным, при его объявлении нужно указать ключевое слово virtual. Рассмотрим, как реализован механизм «позднего связывания» в случае простого наследования и механизм вызова виртуальной функции: как только в объявлении класса появляется виртуальная функция, компилятор для данного класса (а также для каждого производного) в единственном экземпляре формирует таблицу виртуальных функций vtab, в которую с известным ему смещением заносит адреса всех виртуальных методов класса. Перегруженные виртуальные функции имеют одинаковые смещения в таблицах виртуальных функций для разных классов, связанных наследованием; При создании каждого экземпляра такого класса компилятор выделяет дополнительную память. Теперь каждый экземпляр класса содержит не только поля, предназначенные для хранения данных, по и служебное поле для хранения указателя на таблицу виртуальных функций данного класса – vptr. Так как программист всегда явно указывает, объект какого класса он хочет создать, в самом объекте vptr всегда содержит адрес «своей» таблицы виртуальных функций. Вызов виртуальной функции посредством адреса объекта в отличие от обычного метода класса происходит поэтапно: а) зная адрес объекта и местоположение в объекте vptr (указателя на таблицу виртуальных функций) и смещение адреса требуемой функции в таблице vtab, компилятор вычисляет адрес виртуальной функции в таблице виртуальных функций очевидным образом: vptr[смещение]; б) по вычисленному адресу передает управление (таким образом осуществляется вызов виртуальной функции целевого класса во время выполнения), передавая в качестве дополнительного параметра, как и в случае обычных методов класса, адрес того объекта, для которого вызывается виртуальная функция. Ø Замечание: при вызове виртуальной функции компилятор не знает, какого типа объект (ему эта информация не нужна). Для вызова компилятору достаточно располагать следующей информацией: адрес объекта, расположение в объекте указателя vptr и смещение адреса требуемого метода в таблице vtab. 16. 02. 2018 31

 • Пример 5: Виртуальные деструкторы #include <iostream> using namespace std; class Animal { • Пример 5: Виртуальные деструкторы #include using namespace std; class Animal { protected: int age; // возраст public: Animal(){age = 1; } Animal(int Age){age = Age; } virtual ~Animal(){} virtual void Voice() {cout << "? ? ? "; } }; class Dog : public Animal { public: Dog(int Age): Animal(Age){}; Dog(): Animal(){}; virtual void Voice(){cout << "n. Гав!"; }; virtual ~Dog(){} }; 16. 02. 2018 32

 • Пример 5: Виртуальные деструкторы class Cat : public Animal { public: Cat(int • Пример 5: Виртуальные деструкторы class Cat : public Animal { public: Cat(int Age): Animal(Age){}; Cat(): Animal(){}; virtual void Voice(){cout << "n. Мяу!"; }; virtual ~Cat(){} }; int main() { Animal* zoo[] = {new Animal(), new Cat(5), new Dog(6), new Cat(2)}; for (int i=0; iVoice(); delete zoo[i]; } } 16. 02. 2018 33

 Большинство методов класса (кроме конструкторов) могут быть, а иногда и должны быть виртуальными, Большинство методов класса (кроме конструкторов) могут быть, а иногда и должны быть виртуальными, в том числе и деструктор (несмотря на то, что в разных классах имена этих методов разные!!!). В примере 5 предыдущих слайдов, если не делать деструктор базового класса Animal виртуальным, то при выполнении оператора delete zoo[i]; : - при i=0 вызовется деструктор класса Animal; - при i=1 вызовется только деструктор базового класса Animal, поэтому производная часть объекта класса Cat не будет корректно освобождена; - при i=2 вызовется только деструктор базового класса Animal, поэтому производная часть объекта класса Dog не будет корректно освобождена; - при i=3 вызовется только деструктор базового класса Animal, поэтому производная часть объекта класса Cat не будет корректно освобождена; Виртуальные деструкторы нужны для корректного освобождения памяти из под производной части объектов производных классов, если работа с объектами производных классов выполняется через указатели на базовый класс. Если нужно использовать указатель на базовый класс для доступа к методам производного класса, то указатель на базовый класс следует привести к типу производного класса. Для этого используется оператор dynamic_cast(pointer) (пример 6 на следующем слайде). Оператор dynamic_cast возвращает указатель типа Т*, если преобразование корректно, или 0. 16. 02. 2018 34

 • Пример 6: Динамическое приведение типов class Cat : public Animal { public: • Пример 6: Динамическое приведение типов class Cat : public Animal { public: Cat(int Age): Animal(Age){}; Cat(): Animal(){}; void Murl() {cout << "n. Муррр!"; }; virtual void Voice(){cout << "n. Мяу!"; }; virtual ~Cat(){} }; int main() { Animal* zoo[] = {new Animal(), new Cat(5), new Dog(6), new Cat(2)}; for (int i=0; iVoice(); Cat *p. Real. Cat = dynamic_cast(zoo[i]); if (p. Real. Cat) p. Real. Cat->Murl(); delete zoo[i]; } } 16. 02. 2018 35

 • Пример 7: Чисто виртуальные функции и абстрактные классы #include <iostream> using namespace • Пример 7: Чисто виртуальные функции и абстрактные классы #include using namespace std; class Animal // абстрактный класс { protected: int age; // возраст public: Animal(){age = 1; } Animal(int Age){age = Age; } virtual ~Animal(){} virtual void Voice()= 0; }; class Dog : public Animal { public: Dog(int Age): Animal(Age){}; Dog(): Animal(){}; virtual void Voice(){cout << "n. Гав!"; }; virtual ~Dog(){} }; 16. 02. 2018 36

 Иногда при построении иерархии классов базовый класс является настолько обобщенным, что объектов такого Иногда при построении иерархии классов базовый класс является настолько обобщенным, что объектов такого класса просто не существует. Например, мы создавали экземпляры класса Animal, но на самом деле такого «обобщенного» животного в природе нет, зато есть кошки, собаки … Такой базовый класс описывает общие для всех животных данные (возраст, пол …) и посредством методов базового класса описывается общее для всех животных поведение. Но реализацию некоторых методов (Voice) можно написать только для конкретного животного, а в базовом классе вложить какой-то смысл в такой метод невозможно. Поэтому до сих пор метод Voice базового класса был просто заглушкой (пустое тело функции или «? ? ? » ). Посредством использования чисто виртуальных функций программист может сообщить компилятору, что данный класс является абстракцией, поэтому реализацию такого метода компилятор должен искать в производных классах. Класс, содержащий хотя бы одну чисто виртуальную функцию, называется абстрактным. Последствия: - компилятор не позволит создавать объекты такого класса; - можно пользоваться указателем на абстрактный класс; - компилятор будет следить за тем, чтобы в производных классах чисто виртуальный метод был реализован. Замечание. Класс, содержащий только чисто виртуальные функции, называется классом-протоколом (задает для производных классов только интерфейс без какой-либо реализации и заставляет каждый производный класс иметь собственную реализацию заданного интерфейса). 16. 02. 2018 37

 • Пример 7: Чисто виртуальные функции и абстрактные классы class Cat : public • Пример 7: Чисто виртуальные функции и абстрактные классы class Cat : public Animal { public: Cat(int Age): Animal(Age){}; Cat(): Animal(){}; virtual void Voice(){cout << "n. Мяу!"; }; virtual ~Cat(){} }; int main() { Animal* zoo[] = {new Cat(5), new Dog(6), new Cat(2)}; for (int i=0; iVoice(); delete zoo[i]; } } 16. 02. 2018 38

Базовый класс может предоставлять производному классу только интерфейс (обязать производный класс реализовывать заданное базовым Базовый класс может предоставлять производному классу только интерфейс (обязать производный класс реализовывать заданное базовым классом поведение) - чисто виртуальные методы 16. 02. 2018 реализацию - не виртуальные методы реализацию по умолчанию + интерфейс (возможность модификации реализации) -обычные виртуальные -методы 39

 При открытом наследовании базовый класс может предоставлять производным: реализацию - некоторую общую для При открытом наследовании базовый класс может предоставлять производным: реализацию - некоторую общую для всех потомков функциональность (то есть не предполагается, что в производном классе в указанное базовым классом действие можно вложить другой смысл). Например: метод базового класса (пример 8 следующего слайда) void Animal: : Inc. Year(){age++; } не может вести себя по-другому в производных классах; интерфейс – только обозначение действия, так как базовый класс в таком случае «знает» , что такое действие должно иметь место в производных классах, но «не знает» как такое действие реализовать в базовом. Например, virtual void Animal: : Voice()=0; реализацию «по умолчанию» + интерфейс (то есть производный класс может пользоваться функциональностью базового или вложить свой собственный смысл в указанное действие). Например: virtual void When. Do. ISleep(){cout << " At nightn"; } // поведение по умолчанию virtual void When. Do. ISleep(){cout << " 12 hours a dayn"; } // производный класс может вложить новый смысл в данное действие, перегрузив виртуальный метод, или пользоваться базовой версией по умолчанию. 16. 02. 2018 40

 • Пример 8. #include <iostream> using namespace std; class Animal { int age; • Пример 8. #include using namespace std; class Animal { int age; public: void Inc. Year(){age++; } virtual void Voice()= 0; virtual void When. Do. ISleep(){cout << " At nightn"; }; virtual ~Animal(){} int get_age(){ return age; } }; class Dog : public Animal { public: virtual void Voice(){cout << "n. Гав!"; }; virtual void When. Do. ISleep(){cout << " 12 hours a dayn"; }; virtual ~Dog(){} }; 16. 02. 2018 41

 • Пример 8. class Cat : public Animal { public: virtual void Voice(){cout • Пример 8. class Cat : public Animal { public: virtual void Voice(){cout << "n. Мяу!"; }; virtual ~Cat(){} }; int main() { Animal* zoo[] = {new Dog(), new Cat()}; for (int i=0; iInc. Year(); cout << "age = " << zoo[i]->get_age() << "n"; zoo[i]->Voice(); zoo[i]->When. Do. ISleep(); delete zoo[i]; } } 16. 02. 2018 42

1. 2. 3. 4. 5. 6. 7. Контрольные вопросы Отношения между классами в С++. 1. 2. 3. 4. 5. 6. 7. Контрольные вопросы Отношения между классами в С++. Простое открытое наследование – понятие, синтаксис объявления производного класса. Примеры. Конструкторы производных классов: синтаксис объявления, примеры. Порядок вызова конструкторов и деструкторов при создании объектов производного класса. Дайте характеристику механизма раннего связывания (перегрузки имен функций). Примеры. Понятие полиморфизма в С++. Как реализуется полиморфизм в С++? Как в С++ реализован механизм «позднего связывания» в случае простого наследования и механизм вызова виртуальной функции? Назначение виртуальных деструкторов. Пример использования. Понятие, назначение и синтаксис описания чистых виртуальных функций и абстрактных классов. Примеры. 43