Тема 2_1 Отношения между классами и объектами.ppt
- Количество слайдов: 48
Основы объектно-ориентированного программирования на С++ Тема 2. 1 Отношения между классами и объектами 06. 02. 2018 1
Наследование. Простое наследование Базовый класс А Поле а Методы класса А Уровень 0 Уровень 1 Производный класс C Поле c Методы класса C Производный класс В Поле b Методы класса B Уровень 2 Производный класс D Поле d Методы класса D A Поле а B Поле а Поле b Производный класс E Поле e Методы класса E C Поле а Поле c D Поле а Поле b Поле d Производный класс F Поле f Методы класса F E Поле а Поле b Поле e 06. 02. 2018 F Поле а Поле c Поле f 2
Ø Ø Ø Ø Наследование поддерживает между классами (и соответственно между экземплярами этих классов т. е. объектами) отношение «is-a» ( «является разновидностью» ). Базовый класс описывает общие поля данных и реализует только самые общие методы, описывающие поведение объектов его иерархии. Производные классы всегда является разновидностью базового, т. е. каждый производный класс использует для описания структуры и поведения объектов поля данных и методы базового класса и добавляет свои специфические поля данных и методы. Наследование – это один из инструментов языка С++ (и других объектно-ориентированных языков программирования), позволяющих создавать новые классы на основе существующих (производные классы создаются на основе базовых). С помощью механизма наследования в С++ реализуется возможность повторного использования кода. Унаследованные поля и методы базового класса могут использоваться объектами производных классов. Язык С++ допускает как простое, так и множественное наследование. Наследование называют простым, когда каждый класс-потомок имеет только один родительский класс ближайшего уровня. На рисунке представлена иерархия классов при простом наследовании (три уровня иерархии). Ниже на рисунке показано сколько будет зарезервировано памяти под объекты каждого класса. 06. 02. 2018 3
Простое наследование • Синтаксис объявления производного класса class Имя_производного_класса : спецификатор_доступа Имя_базового_класса { поля данных, методы }; Спецификатор доступа public Спецификатор доступа элементов базового класса public в производном классе public protected в производном классе protected private в производном классе private protected в производном классе protected private в производном классе невидим в производном классе private 06. 02. 2018 4
Ø Ø Ø При наследовании каждый производный класс определяет права доступа к своим элементам и к элементам, унаследованным от родителя, а также возможность доступа к ним для следующих в иерархии производных классов. Это реализуется с помощью тех же ключевых слов private, public и protected. Базовый класс скрывает свои элементы private от всех производных классов в том смысле, что объекты производных классов могут использовать их только через доступные им методы самого базового класса. Элементы protected базового класса доступны в производном классе, а элементы public доступны везде. Однако эти права могут изменяться производным классом для классов более низкого уровня иерархии данного базового класса: Ø Если производный класс наследует базовый с ключом доступа public, то права доступа к унаследованным элементам базового класса определяются самим базовым классом как для текущего производного класса, так и для производных классов следующего уровня в иерархии; Ø если производный класс наследует базовый с ключом доступа private, то все элементы, унаследованные от базового класса, скрываются для следующих потомков и интерпретируются как private в объектах текущего производного класса, даже если в базовом классе они были public или protected; Ø если производный класс наследует базовый с ключом доступа protected, то он тем самым разрешает унаследованный доступ к элементам protected в следующих потомках, но закрывает для них public – элементы, превращая их в protected. Эти правила распространяются как на поля данных, так и на методы классов. Чаще всего на практике используют public – наследование (открытое наследование). В частности, если используются конструкторы с параметрами, то наследование должно быть открытое, чтобы была возможность создавать объекты производных классов. 06. 02. 2018 5
Простое наследование • Синтаксис объявления конструктора производного класса Имя конструктора_производного_класса (Список_формальных_параметров ) : имя_базового_класса ( Список_параметров) { // Тело конструктора производного класса } ; 06. 02. 2018 6
Ø Ø Ø Использование конструкторов и деструкторов в классах, являющихся элементами иерархии, имеет некоторые особенности. Дополнительно к обычным правилам, для конструкторов производных классов необходимо учитывать следующее: Объекты производных классов как бы состоят из двух частей: одна часть унаследована от базового класса, а другую часть представляет собственно производный класс. Поэтому при создании и инициализации объектов производных классов используются конструкторы и базового и производного классов. Причем, сначала вызывается конструктор соответствующего базового класса для инициализации той части объекта производного класса, которая унаследована от базового класса, а потом конструктор производного класса. Конструктор производного класса инициализирует часть объекта производного класса, описанную в производном классе. Если базовый класс имеет несколько конструкторов, то необходимый выбор указывается в реализации конструктора производного класса. При этом если конструктор базового класса требует параметры, то они включаются и в список параметров конструктора производного класса. При уничтожении объектов производных классов тоже вызываются деструкторы как производного, так и базового классов. Причем сначала вызывается деструктор производного класса, а потом деструктор базового класса. 06. 02. 2018 7
• Пример1: Создание производного класса. и использование объектов #include <iostream> #include <conio. h> using namespace std; enum BREED {YORKIE, SHETLAND, DOBERMAN}; class Mammal // объявление класса млекопитающее { protected: int its. Age; // возраст int its. Weight; // вес public: Mammal(); //стандартный конструктор Mammal(int age); //конструктор с параметром ~Mammal(); 06. 02. 2018 8
// методы доступа int Get. Age()const {return its. Age; } void Set. Age(int age){its. Age = age; } int Get. Weight()const {return its. Weight; } void Set. Weight(int weight){its. Weight = weight; } // другие методы void Speak()const // голос { cout << " Mammal sound!n "; } void Sleep() // спать { cout << " Shhh. I’m sleepingn "; } }; 06. 02. 2018 9
// объявление производного класса собака class Dog : public Mammal { private: BREED its. Breed; // порода public: Dog(); Dog(int age, int weight); ~Dog(); int Get. Breed()const{return its. Breed; } void Set. Breed(BREED breed){its. Breed = breed; } void Wag. Tail(){cout << " Tail wagging. . n "; } void Beg. For. Food(){cout << " Begging for food. . n "; } }; 06. 02. 2018 10
// определения конструкторов и деструктора // базового класса Mammal: : Mammal(): its. Age(1), its. Weight(5) { cout << " Mammal constructor!n"; } Mammal: : Mammal(int age): its. Age(age), its. Weight(5) { cout << " Mammal (int) constructor!n"; } Mammal: : ~ Mammal() { cout << " Mammal destructor!n"; getch(); } 06. 02. 2018 11
// определения конструкторов и деструктора // производного класса Dog: : Dog(): Mammal(), its. Breed(YORKIE) { cout << "Dog constructor!n"; } Dog: : Dog(int age): Mammal(age), its. Breed(YORKIE) { cout << "Dog (int) constructor!n"; } Dog: : Dog(int age, int weight): Mammal(age), its. Breed(YORKIE) { its. Weight = weight; cout << "Dog (int, int) constructor!n"; } Dog: : ~Dog() { cout << "Dog destructor!n"; } 06. 02. 2018 12
int main() { Dog Sharik; Dog Tuzik(5); Dog Reks(6, 8); Sharik. Speak(); Tuzik. Wag. Tail(); cout << " Reks is " << Reks. Get. Age()<< " years oldn"; return 0; } 06. 02. 2018 13
Множественное наследование Базовый класс Вas 1 Поле i Методы класса Bas 1 Базовый класс Bas 2 Поле j Методы класса Bas 2 Производный класс D Поле k Методы класса D 06. 02. 2018 14
Множественное наследование • Синтаксис объявления производного класса class Имя_производного_класса : спецификатор_доступа Имя_базового_класса_1, спецификатор_доступа Имя_базового_класса_2, … спецификатор_доступа Имя_базового_класса_N, { поля данных, методы }; 06. 02. 2018 15
Множественное наследование • Синтаксис объявления конструктора производного класса Имя конструктора_производного_класса (Список_формальных_параметров): имя_базового_класса 1 ( Список_параметров 1 ), имя_базового_класса 2 ( Список_параметров 2 ), . . . , имя_базового_класса. N (Список_параметров. N ) { // Тело конструктора производного класса } ; 06. 02. 2018 16
Ø Наследование называют множественным, когда несколько базовых классов используют для создания нового класса-потомка, наследуемого свойства всех своих родительских классов. При использовании множественного наследования дерево классов может превратиться в сложный граф. Ø Синтаксис объявления производного класса при множественном наследовании: после имени производного класса ставится двоеточие, за которым следует список указанных через запятую имен базовых классов, каждое из которых предваряется спецификатором доступа (ключевым словом public, protected или private, определяющим доступ к данным базового класса внутри производного). Ø Объявление базового класса производится обычным способом. Ø Конструктору производного класса при множественном наследовании, помимо собственных параметров, следует передать все параметры, необходимые конструкторам всех базовых классов. Ø В списке инициализации конструктора производного класса явно вызываются конструкторы базовых классов, которым передаются требуемые значения параметров. Ø При создании объекта производного класса конструкторы вызываются слева направо в порядке записи базовых классов при объявлении производного класса. 06. 02. 2018 17
Пример 2. Наследование от двух базовых классов #include <iostream> #include <conio. h> using namespace std; class Bas 1 // объявление базового класса { int i; public: Bas 1(int n) {i = n; } int get_i() { return i; } }; class Bas 2 // объявление базового класса { int j; public: Bas 2(int n){j = n; } int get_j() { return j; } }; 06. 02. 2018 18
// объявление производного класса class D : public Bas 1, public Bas 2 { int k; public: // p и m передаются в Bas 1 и Bas 2 D(int n, int m, int p): Bas 1(p), Bas 2(m){k = n; } void show() { cout << get_i()<<' ' << get_j()<<' '<< k; } }; void main(void) { D ob(50, 70, 90); ob. show(); // 90 70 50 getch(); } 06. 02. 2018 19
Полиморфизм. Статический полиморфизм Пример 3. Переопределение метода базового класса в производном классе: #include <iostream> #include <conio. h> using namespace std; enum BREED {YORKIE, SHETLAND, DOBERMAN}; class Mammal // Класс млекопитающее { protected: int its. Age; // возраст int its. Weight; // вес 06. 02. 2018 20
public: Mammal(){cout <<"Mammal constructor!n"; } ~ Mammal() { cout << "Mammal destructor!n"; getch(); } void Speak()const // голос { cout << " Mammal sound!n "; } void Sleep() // спать { cout << " Shhh. I’m sleepingn "; } }; 06. 02. 2018 21
// Производный класс собака class Dog : public Mammal { private: BREED its. Breed; // порода public: Dog(){cout << " Dog constructor!n"; } ~ Dog(){ cout << " Dog destructor!n"; } // другие методы void Speak()const // голос { cout << " Woof!n "; } void Wag. Tail(){cout << " Tail wagging. . n "; } void Beg. For. Food(){cout << " Begging for food. . n "; } }; 06. 02. 2018 22
int main() { Mammal big. Animal; Dog Sharik; big. Animal. Speak(); Sharik. Speak(); return 0; } 06. 02. 2018 23
Ø Ø Ø Ø Ø Слово "полиморфизм" греческого происхождения и означает "имеющий много форм". Полиморфизм в ООП - это придание одного и того же имени действию, которое выполняется над различными объектами классов, находящихся в иерархическом подчинении. Для каждого из объектов это действие может выполняться по своему. Различают статический полиморфизм, базирующийся на механизме раннего связывания, (поддерживается на этапе компиляции), и динамический полиморфизм, использующий механизм позднего связывания (поддерживается на этапе выполнения программы). Статический полиморфизм реализуется в С++ с помощью механизмов переопределения и перегрузки. Перегружать в С++ можно методы класса, обычные функции и операции. Перегружаемые функции и методы, в соответствии с общими правилами, могут отличаться типом возвращаемого параметра и сигнатурой ( количеством и типом передаваемых параметров). Переопределять в С++ можно методы класса. Переопределяемый метод базового класса и переопределяющий его метод производного класса должны совпадать не только по именам, но также по типу возвращаемого параметра и сигнатуре. В программе примера 3 метод Speak() класса Dog переопределяет одноименный метод базового класса. В базовом классе метод Speak() один и поэтому при переопределении его методом производного класса проблем не возникает. Если в базовом классе переопределяемый метод является перегруженным, то тогда могут возникнуть проблемы. Например, если класс Mammel имеет перегруженный метод Move() (двигаться), который в классе Dog будет переопределен, то метод класса Dog скроет все одноименные методы класса Mammel (т. е. они становятся недоступными объектам производного класса). Эту проблему иллюстрирует программа примера 4. 06. 02. 2018 24
Статический полиморфизм Пример 4. Переопределение перегруженного метода базового класса в производном классе: #include <iostream> #include <conio. h> class Mammal // Класс млекопитающее { protected: int its. Age; // возраст int its. Weight; // вес public: void Move() const { std: : cout << "Mammal move one stepn "; } void Move(int distance)const { std: : cout << "Mammal move " << distance << " steps. n "; } }; 06. 02. 2018 25
class Dog : public Mammal { public: void Move() const { std: : cout << " Mammal move 5 stepsn "; } }; int main() { Mammal big. Animal; Dog Sharik; big. Animal. Move(); big. Animal. Move(2); Sharik. Move(); // Sharik. Move(10); getch(); return 0; } 06. 02. 2018 26
Ø Ø Ø Базовый класс Mammal содержит перегруженные методы Move(). Производный класс Dog переопределяет метод Move(), который не имеет параметров. Строку Sharik. Move(10); пришлось закомментировать, поскольку она приводила к ошибке при компиляции. Логично было бы предположить, что в классе Dog можно использовать метод Move(int), поскольку переопределен лишь метод Move() без параметров, но в действительности, чтобы использовать метод Move(int), его также необходимо переопределить в классе Dog. В противном случае не переопределенный метод окажется скрыт. Но все же переопределенный метод базового класса остается доступным, если указать его полное имя. Полное имя члена класса состоит из имени класса, двойного двоеточия и имени метода. 06. 02. 2018 27
class Dog : public Mammal { public: void Move() const { std: : cout << " Mammal move 5 stepsn "; } }; int main() { Mammal big. Animal; Dog Sharik; big. Animal. Move(); big. Animal. Move(2); Sharik. Move(); Sharik. Mammal: : Move(10); getch(); return 0; } 06. 02. 2018 28
Указатели на базовый и производные классы base *p; // указатель на объект //базового класса base b_ob; // объект базового класса derived d_ob; // объект производного класса … p = &b_ob; // указатель на объект базового класса // может содержать адрес объекта производного // класса p = &d_ob; 06. 02. 2018 29
Динамический полиморфизм Часть Mammal Объект класса Dog VPTR &Move Mammal VPTR &Speak Mammal &Mammal: Move() &Dog: Speak() Dog Таблица виртуальных функций класса Mammal Таблица виртуальных функций класса Dog 06. 02. 2018 30
Ø Ø Ø Ø Динамический полиморфизм реализуется в С++ с использованием указателя на базовый класс и механизма переопределения виртуальных функций : если несколько различных классов являются производными от базового, содержащего виртуальную функцию, то если указатель на базовый класс ссылается на разные объекты этих производных классов, то выполняются разные версии виртуальной функции (т. е. переопределенные виртуальные функции соответствующих производных классов). Виртуальной называется функция, вызов которой зависит от типа объекта. Виртуальные функции всегда являются методами класса. Для объявления функции как виртуальной используется ключевое слово virtual. При переопределении виртуальной функции в производном классе, ключевое слово virtual не требуется. Но несмотря на это, некоторые программисты явно объявляют функции виртуальными на каждом уровне иерархии, чтобы обеспечить ясность программы. Обычно компиляторы при обработке виртуальных функций добавляют к каждому объекту скрытый элемент, который содержит указатель на массив адресов функций, называемый таблицей виртуальных функций (vtbl). Таблица содержит адреса виртуальных функций, объявленных для объектов этого класса. Когда создается объект производного класса (например, Dog), сначала происходит вызов конструктора базового класса, а затем – производного. На рисунке показано, как будет выглядеть объект класса Dog после создания. При размещении в памяти та часть объекта, которая относиться к классу Mammal, неразрывна с частью относящейся к классу Dog. Каждый объект обладает указателем vptr, который ссылается на таблицу виртуальных функций, содержащую указатели на все виртуальные функции. Указатель vptr объекта класса Dog инициализируется при создании части объекта, принадлежащей базовому классу Mammal, как показано на рисунке. Когда происходит вызов конструктора Dog() и к созданному объекту добавляется часть Dog, указатель vptr корректируется так, чтобы он указывал на виртуальные функции (если они есть), переопределенные в классе Dog. 06. 02. 2018 31
Динамический полиморфизм Пример 5. Использование указателей на базовый класс и виртуальных функций: #include <iostream> #include <conio. h> using namespace std; class Mammal // Класс млекопитающее { protected: int its. Age; // возраст public: Mammal(): its. Age(1) {} virtual ~Mammal(){} virtual void Speak() {cout << "Mammal speak!n "; } }; • 06. 02. 2018 32
class Dog : public Mammal { public: void Speak(){cout << "Woof!n"; } }; class Cat : public Mammal { public: void Speak(){cout << "Meow!n"; } }; class Horse : public Mammal { public: void Speak(){cout << "Winnie!n"; } }; class Pig : public Mammal { public: void Speak(){ cout << "Oink!n"; } }; 06. 02. 2018 33
int main() {Mammal* the. Array[5]; //массив указателей Mammal* ptr; int choice, i; for (i=0; i<5; i++) {std: : cout<<"(1)dog (2)cat (3)horse (4)pig: "; std: : cin >> choice; switch(choice) { case 1: ptr = new Dog; break; case 2: ptr = new Cat; break; case 3: ptr = new Horse; break; case 4: ptr = new Pig; break; default: ptr = new Mammal; break; } the. Array[i]=ptr; } 06. 02. 2018 34
for (i=0; i<5; i++) the. Array[i]->Speak(); getch(); return 0; } 06. 02. 2018 35
Ø Ø Ø Ø Допустим необходимо моделировать фермерское хозяйство, состоящее из различного вида млекопитающих (например, Dog (собака), Cat(кот), Horse(лошадь) и Pig (свинья)). Для этого необходимо определить массив объектов классов, производных от класса Mammal. При моделировании фермы каждое животное должно быть подавать свой голос, т. е. собака должна лаять, кот мяукать, и т. д. Поэтому метод Speak() в базовом классе необходимо объявить виртуальным и переопределить его в каждом производном классе. Цикл в программе примера 5 запрашивает у пользователя тип объекта производного класса и добавляет в массив указателей на базовый класс Mammal адрес на вновь созданный объект. Обратите внимание: на момент компиляции неизвестно, какие именно объекты будут созданы, а, следовательно, какие из методов Speak() использованы. Указатель ptr привязывается к своему объекту только в процессе выполнения. Это и есть позднее связывание, или связывание в процессе выполнения программы, в отличие от раннего (статического) связывания, или связывания в процессе компиляции программы. При работе с объектами производных классов через указатель на базовый класс может произойти ошибка при уничтожении объектов производных классов. Базовый класс ничего не знает о деструкторах производных классов, поэтому будет вызван деструктор базового класса перед уничтожением части объекта производного класса, унаследованной от базового класса. Деструкторы производных классов вызваны не будут. Чтобы этой ошибки не было деструкторы базовых классов всегда следует объявлять виртуальными. Если базовый класс содержит виртуальный деструктор, то деструктор производного класса тоже будет виртуальным. Если нужно использовать указатель на базовый класс для доступа к специфичным методам производного класса, то указатель на базовый класс следует привести к типу производного класса. Для этого используется оператор dynamic_cast (пример 6). Коты мурлычут, а остальные млекопитающие - нет. Если добавить в производный класс Cat метод Purr() (мурлыканье), то компилятор сообщит об ошибке в том случае, если вызвать метод Purr(), используя указатель на базовый класс Mammal не приводя его к типу указателя на производный класс Cat. 06. 02. 2018 36
Пример 6. Динамическое приведение типов: #include <iostream> #include <conio. h> class Mammal // Класс млекопитающее { protected: int its. Age; // возраст public: Mammal(): its. Age(1) {} virtual ~Mammal(){} virtual void Speak() const { std: : cout << "Mammal speak!n "; } }; 06. 02. 2018 37
class Dog : public Mammal { public: void Speak() const { std: : cout << "Woof!n"; } }; class Cat : public Mammal { public: void Speak() const { std: : cout << "Meow!n"; } void Purr() const { std: : cout << "rrrrrr!n"; } }; 06. 02. 2018 38
int main() {Mammal* Zoo[3]; Mammal* p. Mammal; int choice, i; for (i=0; i<3; i++) {std: : cout<<"(1)dog (2)cat: "; std: : cin >> choice; if (choice == 1) p. Mammal = new Dog; else p. Mammal = new Cat; Zoo[i] = p. Mammal; } std: : cout << "n"; 06. 02. 2018 39
for (i=0; i<3; i++) { Zoo[i]->Speak(); Cat *p. Real. Cat = dynamic_cast<Cat*>(Zoo[i]); if (p. Real. Cat) p. Real. Cat->Purr(); else std: : cout << "Uh oh, not a cat!n"; delete Zoo[i]; std: : cout << "n"; } getch(); return 0; } 06. 02. 2018 40
Абстрактные классы и чисто виртуальные функции Синтаксис объявления чисто виртуальной функции: virtual тип имя_функции (список_параметров) = 0 06. 02. 2018 41
Ø Ø Чисто виртуальная функция – это виртуальная функция, тело которой не определено. Она используется для того, чтобы отложить выбор реализации функции. Класс, имеющий хотя бы одну чисто виртуальную функцию, называется абстрактным классом. Абстрактный класс должен задавать основные общие свойства для всех производных классов, но сам не может использоваться для объявления объектов. Но он может применяться для объявления указателей, которые будут хранить адреса объектов производных классов от данного абстрактного класса. Благодаря этому достигается динамический полиморфизм. Если существует класс, производный от абстрактного класса и чисто виртуальная функция в нем не определена, то эта функция остается чисто виртуальной и в производном классе, а сам класс также является абстрактным. 06. 02. 2018 42
Пример 7. Абстрактные классы и чисто виртуальные функции #include <iostream> class Shape { public: virtual double area() const = 0; }; class Square: public Shape { public: Square(double a = 1): _a(a) { } virtual double area() const { return _a * _a; } private: double _a; }; class Circle: public Shape { public: Circle(double r = 1): _r(r) { } virtual double area() const { return 3. 14159265358 * _r; } private: double _r; }; 06. 02. 2018 43
int main() { Shape *shapes[5]; shapes[0] = new Circle (3); shapes[1] = new Square (2); shapes[2] = new Square (2. 5); shapes[3] = new Circle (5); shapes[4] = new Circle (10); for (int i = 0; i < 5; ++i) std: : cout << shapes[i]->area() << std: : endl; for (int i = 0; i < 5; ++i) delete shapes[i]; return 0; } 06. 02. 2018 44
Виртуальные базовые классы class Parent { potected: int data. Parent; }; class Child 1: public Parent {}; class Child 2: public Parent {}; class Grand. Child: public Child 1, public Child 2 { public: Parent int getdata() // ошибка !!! {return data. Parent; } Child 1 Child 2 }; Grand. Child 06. 02. 2018 45
Ø Ø Базовые классы и классы, производные от них, могут образовывать довольно сложные иерархии, работа с которыми может привести к возникновению неопределенных ситуаций. В представленной на рисунке иерархии могут возникнуть проблемы, связанные с неоднозначностью, если класс Grand. Child захочет получить доступ к элементам класса Parent. Неоднозначность возникает вследствие того, что каждый из производных классов Child 1 и Child 2 создает собственную копию унаследованного поля data. Parent базового класса Parent. Поэтому при обращении производного класса Grand. Child к полю data. Parent непонятно, к какой именно копии будет происходить обращение. Если объявить базовые классы Child 1 и Child 2 виртуальными, неоднозначность устраняется, поскольку копии объекта не создаются. 06. 02. 2018 46
Виртуальные базовые классы class Parent { potected: int data. Parent; }; class Child 1: virtual public Parent {}; class Child 2: virtual public Parent {}; class Grand. Child: public Child 1, public Child 2 { public: Parent int getdata() {return data. Parent; } }; Child 1 Child 2 Grand. Child 06. 02. 2018 47
1. 2. 3. 4. 5. 6. 7. Контрольные вопросы Понятие наследования. Простое наследование – понятие, синтаксис объявления производного класса. Конструкторы производных классов: синтаксис объявления, особенности использования. Множественное наследование – понятие, синтаксис объявления производного класса, пример использования. Понятие и виды полиморфизма в С++. Статический полиморфизм в С++: понятие, механизмы реализации, примеры использования. Динамический полиморфизм в С++: понятие, механизмы реализации, примеры использования. Как организовать использование указателя на базовый класс для доступа к методам (не виртуальным) производного класса ? В каком случае деструкторы объявляются виртуальными? Понятие и назначение чистых виртуальных функций и абстрактных классов. Виртуальные базовые классы: назначение, пример использования. 48
Тема 2_1 Отношения между классами и объектами.ppt