C++. Спадковість. Порядок виклику конструкторів при спадковості

Порядок виклику конструкторів при спадковості. Обмеження спадковості. Властивості покажчика (посилання) на базовий клас

Дана тема є продовженням теми:


Зміст


Пошук на інших ресурсах:

1. Виклик конструкторів класу при спадковості

Якщо два класи утворюють ієрархію спадковості, то при створенні екземпляру похідного класу спочатку викликається конструктор базового класу який конструює об’єкт похідного класу. Потім цей конструктор стає недоступним і доповнюється кодом конструктора похідного класу. Таким чином, спочатку відбувається ініціалізація даних базового класу, потім відбувається ініціалізація даних похідного класу.
Якщо класи, що утворюють ієрархію, то деструктори цих класів викликаються у зворотному порядку по відношенню до виклику конструкторів. Спочатку викликається деструктор похідного класу, потім викликається деструктор базового класу.
На рисунку 1 зображено порядок виклику конструкторів для двох класів A, B які утворюють ієрархію спадковості для випадку створення екземпляру похідного класу B.

C++. Спадковість. Порядок виклику конструкторів для випадку двох класів

Рисунок 1. Порядок виклику конструкторів для випадку двох класів: 1 – конструктор класу A; 2 – конструктор класу B; 3 – деструктор класу B; 4 – деструктор класу A

 

2. Приклад, що демонструє порядок виклику конструкторів. Розглядаються 3 класи

У прикладі оголошується 3 класи A, B, C які утворюють ієрархію спадковості. У класах міститься по одному конструктору та деструктору. З метою візуалізації, код конструкторів та деструкторів містить виведення відповідної інформації.

#include <iostream>
using namespace std;

// Базовий клас
class A
{
public:
  // Конструктор класу A
  A()
  {
    cout << "Constructor A::A()" << endl;
  }

  // Деструктор класу A
  ~A()
  {
    cout << "Destructor A::~A()" << endl;
  }
};

// Похідні класи
class B : public A
{
public:
  // Конструктор
  B()
  {
    cout << "Constructor B::B()" << endl;
  }

  // Деструктор
  ~B()
  {
    cout << "Destructor B::~B()" << endl;
  }
};

class C :public B
{
public:
  // Конструктор
  C()
  {
    cout << "Constructor C::C()" << endl;
  }

  // Деструктор
  ~C()
  {
    cout << "Destructor C::~C()" << endl;
  }
};

void main()
{
  // Створити екземпляр класу C
  C obj; // A() => B() => C()
} // ~C() => ~B() => ~A()

Результат виконання програми

Constructor A::A()
Constructor B::B()
Constructor C::C()
Destructor C::~C()
Destructor B::~B()
Destructor A::~A()

На основі отриманого результату можна зробити наступні висновки:

  • конструктори викликаються у порядку від базового класу A до успадкованого класу C, що знаходиться на найнижчому рівні ієрархії;
  • деструктори викликаються у зворотному порядку по відношенню до виклику конструкторів.

 

3. Передача аргументів у базовий клас при виклику конструкторів

Якщо в базовому класі є конструктор, який отримує параметри, то при створенні екземпляру похідного класу потрібно передати відповідні аргументи в конструктор базового класу. Тобто потрібно ініціалізувати дані базового класу у конструкторі похідного класу.

Така ініцалізація здійснюється з допомогою спеціального синтаксису, який має наступний вигляд:

// Базовий клас
class Base
{
  // Конструктор базового класу, який отримує параметри
  Base(params_Base)
  {
    // ...
  }
}

// Похідний клас
class Derived : Base
{
  // Конструктор похідного класу - викликає конструктор базового класу
  Derived(params_Derived) : Base(args_Base)
  {
    // ...
  }
}

тут

  • Base, Derived – відповідно базовий та похідний класи;
  • params_Base – параметри, що отримує конструктор базового класу;
  • arg_Base – аргументи, що передаються у конструктор базового класу;
  • params_Derived – параметри, що отримує конструктор похідного класу.

Якщо в базовому класі Base немає взагалі конструктора або є тільки один конструктор без параметрів Base() (конструктор за замовчуванням), то не потрібно звертатись до конструктора базового класу Base з конструктора похідного класу Derived.

 

4. Приклад, що демонструє передачу аргументів у конструктор базового класу

У прикладі оголошуються два класи:

  • Point – базовий клас, що містить один параметризований конструктор. Конструктор отримує 2 параметри;
  • PointColor – клас, що успадкований від класу Point і розширює клас Point властивістю color (колір точки). Конструктор класу PointColor викликає конструктор базового класу Point з допомогою спеціального синтаксису мови C++.

 

#include <iostream>
using namespace std;

// Клас, що описує точку
class Point
{
private:
  double x, y; // координати точки

public:
  // Конструктор, що отримує 2 параметри
  Point(double _x, double _y) : x(_x), y(_y)
  { }

  // Методи доступу
  double X() { return x; }
  double Y() { return y; }
};

// Клас, що успадковує клас Point,
// цей клас додає колір до класу Point
class PointColor : public Point
{
private:
  int color; // додаткове поле - колір

public:
  // Конструктор, що отримує 3 параметри,
  // цей конструктор викликає конструктор базового класу Point(_x, _y)
  PointColor(double _x, double _y, int _color) : Point(_x, _y)
  {
    color = _color;
  }

  // Метод доступу
  int Color() { return color; }
};

void main()
{
  // Створити екземпляр класу PointColor.
  // Викликаються конструктори: Point(5, 8) => PointColor(2)
  PointColor pc(5, 8, 2);

  cout << "pc.x = " << pc.X() << endl;
  cout << "pc.y = " << pc.Y() << endl;
  cout << "pc.color = " << pc.Color() << endl;
}

У вищенаведеному коді, в класі PointColor конструктор цього класу викликає конструктор базового класу Point передаючи йому два аргументи (координати x, y)

// Конструктор, що отримує 3 параметри,
// цей конструктор викликає конструктор базового класу Point(_x, _y)
PointColor(double _x, double _y, int _color) : Point(_x, _y)
{
  color = _color;
}

Третій параметр _color ініціалізує внутрішню змінну color.

Після запуску програма видасть наступний результат

pc.x = 5
pc.y = 8
pc.color = 2

 

5. Елементи класу, які не можуть бути успадковані

У C++ не всі елементи класу можна успадковувати, оскільки вони несумісні з самою ідеєю успадкування. До цих елементів можна віднести:

  • конструктори (всі види конструкторів);
  • деструктор;
  • перевантажені оператори new;
  • перевантажені оператори присвоєння (=);
  • дружні функції або відношення дружньості. Це означає, що якщо базовий клас має дружні функції, то ці функції не є дружніми в успадкованому класі.

 

6. Особливості використання покажчика на базовий клас

Якщо класи утворюють ієрархію спадковості, то для цих класів справедливе наступне правило:

  • покажчик (посилання) на базовий клас може вказувати на екземпляр цього класу а також на екземпляр будь-якого похідного класу в ієрархії успадкування.

Або іншими словами, покажчик (посилання) на базовий клас може бути приведений до типу будь-якого похідного класу в ієрархії успадкування.

Завдяки цьому правилу забезпечується виконання механізму поліморфізму. Більш детально про поліморфізм можна прочитати тут.

На рисунку 2 наведено приклад ієрархії та можливі присвоєння покажчику значень адрес екземплярів похідних класів

C++. Правило покажчика на базовий клас

Рисунок 2. Покажчик p на базовий клас A може вказувати на екземпляри похідних класів, що успадковані від базового класу A

 

7. Приклад, що демонструє використання покажчика на базовий клас

У прикладі оголошується два класи Base та Derived. Класи утворюють ієрархію спадковості. Клас Derived успадковує клас Base.

// Базовий клас
class Base
{
  // ...
};

// Похідний (успадкований) клас
// тут ключове слово public - обов'язкове
class Derived : public Base
{
  // ...
};

void main()
{
  // 1. Оголосити покажчик на базовий клас
  Base* p1;

  // 2. Оголосити екземпляри базового та похідного класу
  Base obj1;
  Derived obj2;

  // 3. Присвоїти покажчику адреси екземплярів
  p1 = &obj1; // можна
  p1 = &obj2; // можна, тепер p вказує на екземпляр похідного класу

  // 4. Так робити не можна
  Derived* p2; // Покажчик на похідний клас
  p2 = &obj2;  // так можна, типи співпадають: Derived* <= Derived*
  p2 = &obj1; // помилка компіляції, базовий клас не може розширитись
  // до можливостей похідного класу Derived* <= Base*
}

У вищенаведеному прикладі в функції main() оголошується покажчик на базовий клас Base та два екземпляри (об’єкти) класів Base та Derived з іменами відповідно obj1 та obj2. Потім по черзі відбувається присвоєння покажчику p значення адрес екземплярів obj1 та obj2. Присвоєння виконується успішно.
На наступному кроці з метою демонстрації оголошується покажчик p2 на похідний клас Derived. Цей покажчик по черзі приймає значення адрес екземплярів obj2 та obj1. У випадку з екземпляром obj2 присвоєння виконується коректно

Derived* p2;
p2 = &obj2; // тут все працює, типи співпадають

У випадку присвоєння адреси екземпляру obj1

p2 = &obj1;

компілятор видає помилку. Це логічно, оскільки екземпляр obj1 базового класу Base не може бути розширений до можливостей похідного класу Derived тому що він не успадковує цей клас (клас Base не успадковує клас Derived).

 


Споріднені теми