Наследование : общее, частное. Конструкторы и деструкторы.

Наследование

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

Основное назначение механизма наследования — повторное использование кодов, так как большинство используемых типов данных являются вариантами друг друга, и писать для каждого свой класс нецелесообразно. Объекты разных классов и сами классы могут находиться в отношении наследования, при котором формируется иерархия объектов, соответствующая заранее предусмотренной иерархии классов.

Наследование

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

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

Наследуемые компоненты не перемещаются в производный класс, а остаются в базовых классах. Сообщение, обработку которого не могут выполнить методы производного класса, автоматически передается в базовый класс. Если для обработки сообщения нужны данные, отсутствующие в производном классе, то они автоматически берутся из базового класса.

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

Для доступа из производного класса к компонентам базового класса, имена которых повторно определены в производном, используется операция разрешения контекста ::

Для порождения нового класса на основе существующего используется следующая общая форма:

 
 
сlass Имя : МодификаторДоступа  ИмяБазовогоКласса
{ объявление_членов;};

При объявлении порождаемого класса МодификаторДоступа может принимать значения public, private или protected либо отсутствовать. По умолчанию используется значение private.

В любом случае порожденный класс наследует все члены базового класса, но доступ имеет не ко всем. Ему доступны общие (public) члены базового класса и недоступны частные (private).

Для того, чтобы порожденный класс имел доступ к некоторым скрытым членам базового класса, в базовом классе их необходимо объявить со спецификацией доступа защищенные (protected).

Члены класса с доступом protected видимы в пределах класса и в любом классе, порожденном из этого класса.

Общее наследование

При общем наследовании порожденный класс имеет доступ к наследуемым членам базового класса с видимостью public и protected. Члены базового класса с видимостью private – недоступны.

Спецификация доступа внутри класса в порожденном классе вне класса
private +
protected + +
public + + +

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

Пример

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
#include <iostream>
using namespace std;
class student
{
protected:
  char fac[20];
  char spec[30];
  char name[15];
public:
  student(char* f, char* s, char* n);
  void print();
};
class grad_student : public student
{
protected:
  int year;
  char work[30];
public:
  grad_student(char* f, char* s, char* n, char* w, int y);
  void print();
};
student::student(char* f, char* s, char* n)
{
  strcpy_s(fac, strlen(f) + 1, f);
  strcpy_s(spec, strlen(s) + 1, s);
  strcpy_s(name, strlen(n) + 1, n);
}
grad_student::grad_student(char* f, char* s, char* n, char* w, int y) :
  student(f, s, n)
{
  year = y;
  strcpy_s(work, strlen(w) + 1, w);
}
void student::print()
{
  cout << endl << "fac: " << fac << " spec : " << spec
    << " name : " << name;
}
void grad_student::print()
{
  student::print();
  cout << " work : " << work << " year : " << year;
}
int main()
{
  system("chcp 1251");
  system("cls");
  student s((char*)"МТ", (char*)"АМСП", (char*)"Сидоров Иван");
  grad_student stud((char*)"ПС", (char*)"УиТС", (char*)"Иванов Петр", (char*)"Метран", 2000);
  student* p = &s;
  p->print();
  grad_student* gs = &stud;
  student* m;
  gs->print();
  m = gs;
  m->print();
  cin.get();
  return 0;
}

Порожденный класс наследует все данные класса student (строка 13), имеет доступ к protected и public — членам базового класса. В новом классе добавлено два поля данных (строки 16, 17), и порожденный класс переопределяет функцию print() (строки 20, 39-43).

Конструктор для базового класса вызывается в списке инициализации (строка 29).

Результат выполнения

Функция main создаёт объект класса student (строка 48) и объект класса grad_student (строка 49).

Указателю p присваивается ссылка на объект класса student (строка 50), а указателю gs объект класса grad_student (строка 52). При этом функции вывода в строках 51 и 54 корректно отрабатываются для своего класса.

Но что происходит, когда мы присваиваем указателю класса student ссылку на объект класса grad_student (строка 55)? В этом случае происходит преобразование указателей, и в строке 56 вызывается уже функция print() класса student.

Указатель на порожденный класс может быть неявно передан в указатель на базовый класс. А указатель на порожденный класс может указывать только на объекты порожденного класса. То есть обратное преобразование недопустимо.

 
 
grad_student *b;
b = s;

Неявные преобразования между порожденным и базовым классами называются предопределенными стандартными преобразованиями:

  • объект порожденного класса неявно преобразуется к объекту базового класса.
  • ссылка на порожденный класс неявно преобразуется к ссылке на базовый класс.
  • указатель на порожденный класс неявно преобразуется к указателю на базовый класс.

Частное наследование

Порожденный класс может быть базовым для следующего порождения. При порождении private наследуемые члены базового класса, объявленные как protected и public, становятся членами порожденного класса с видимостью private. При этом члены базового класса с видимостью public и protected становятся недоступными для дальнейших порождений.

Цель такого порождения — скрыть классы или элементы классов от использования их в дальнейших порождениях.

При порождении private не выполняются предопределенные стандартные преобразования:

 
 
 
 
 
 
 
 
class grad_student : private student 
{…}
int main() 
{
  …
  m = gs; // ошибка
  …
}

Однако порождение private позволяет отдельным элементам базового класса с видимостью public и protected сохранить свою видимость в порожденном классе. Для этого необходимо

  • в части protected порожденного класса указать те наследуемые члены базового класса с видимостью protected, уточненные именем базового класса, для которых необходимо оставить видимость protected и в порожденном классе;
  • в части public порожденного класса указать те наследуемые члены базового класса с видимостью public, уточненные именем базового класса, для которых необходимо оставить видимость public и в порожденном классе.
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
class X 
{
private:
  int n;
protected:
  int m;
  char s;
public:
  void func(int);
};
class Y : private X 
{
private:
  …
protected:
  …
  X::s;
public:
  …
  X::func();
};

Возможен и третий вариант наследования – с использованием модификатора доступа protected.

Доступ к элементам базового класса из производного класса, в зависимости от модификатора наследования:

Модификатор наследования → public protected private
Модификатор доступа ↓
public public protected private
protected protected protected private
private нет доступа нет доступа нет доступа

Конструкторы и деструкторы при наследовании

Как базовый, так и производный классы могут иметь конструкторы и деструкторы.

Если и у базового и у производного классов есть конструкторы и деструкторы, то конструкторы выполняются в порядке наследования, а деструкторы – в обратном порядке. То есть если

  • А – базовый класс,
  • В – производный из А,
  • С – производный из В

(А-В-С), то при создании объекта класса С вызов конструкторов будет иметь следующий порядок:

  • конструктор класса А
  • конструктор класса В
  • конструктор класса С.

Вызов деструкторов при удалении этого объекта произойдет в обратном порядке:

  • деструктор класса С
  • деструктор класса В
  • деструктор класса А.

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

Конструкторы могут иметь параметры. При реализации наследования допускается передача параметров для конструкторов производного и базового класса. Если параметрами обладает только конструктор производного класса, то аргументы передаются обычным способом. При необходимости передать аргумент из производного класса конструктору родительского класса используется расширенная запись конструктора производного класса:

 
 
 
КонструкторПроизводногоКласса (СписокФормальныхАргументов)
: КонструкторБазовогоКласса (СписокФактическихАргументов)
// тело конструктора производного класса }

Для базового и производного классов допустимо использование одних и тех же аргументов. Возможно, списки аргументов конструкторов производного и базового классов будут различны.

Конструктор производного класса не должен использовать все аргументы, часть предназначены для передачи в базовый класс (строка 29, см. код выше). В расширенной форме объявления конструктора производного класса описывается вызов конструктора базового класса.

6 комментариев к “Наследование”

  1. Виктория

    Здравствуйте! а подскажите, пожалуйста, как я могу объявить неполный наследуемый класс? Например, у меня есть class a b class b, наследуемый откласса а. я хочу неполно объявить класс b перед классом а. как это синтаксически сделать? нигде не могу найти инфу об этом.

    1. Елена Вставская

      Прототипом

      1
      class b;

      А ниже класса А описать полностью класс b

  2. Дмитрий

    Здравствуйте, заранее извиняюсь за свой вопрос, возможно он не совсем корректен. Возможно ли передать аргумент из производного класса конструктору родительского класса в теле производного класса, примерно такой конструкцией:
    КонструкторПроизводногоКласса (СписокФормальныхАргументов)
    { // тело конструктора производного класса
    КонструкторБазовогоКласса (СписокФактическихАргументов)
    }

    1. Елена Вставская

      Да, примерно такой конструкцией воспользоваться можно. См. тело конструктора класса grad_student в статье.

      1. Alexander Chervino

        Добрый день!
        grad_student::grad_student(char *f, char *s, char *n, char *w, int y) :
        student(f, s, n)
        Возможно ли в такой конструкции не тащить аргументы базового класса в аргументы производного? Например аргумент f можно ли его определить в теле производного конструктора и затем передать в базовый?

Оставьте комментарий

Ваш адрес email не будет опубликован. Обязательные поля помечены *

Прокрутить вверх