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

Язык C++ / Наследование

 

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

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

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

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

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

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

 
 
с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, см. код выше). В расширенной форме объявления конструктора производного класса описывается вызов конструктора базового класса.


Назад: Язык C++

Комментариев к записи: 2

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

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

Добавить комментарий

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