Объектно-ориентированное программирование, страница 12

Главное положение, которое иллюстрируется в примере, — все данные и методы базового класса наследованы и доступны в методах производных классов. При public-наследовании все данные и методы остались с теми же атрибутами доступа, какие они имели в родительском классе. Это означает, что данные из protected-секции класса Man остались protected-данными в классе Stud и в классе Prof, а методы из public-секции класса Man остались public-методами в обоих производных классах. При запуске программы мы видим на экране значения унаследованных данных потому, что пользуемся для вывода public-методом Out производного класса, который внутри себя имеет обращение к методу Out родительского класса путем явного указания Man::Out();. Это удобная и обычная практика — пользоваться для вывода унаследованных данных родительской версией функции вывода.

Метод Class существует в классе Man. Он также унанаследован и перекрыт (hidden) производными классами и допускают вызов (внутри методов производных классов):

¨  без уточняющего спецификатора, например, Class();

¨  с уточняющим спецификатором Man, например, Man::Class();

¨  со спецификатором своего класса, например, Stud::Class().

Тела унаследованных методов не повторяются, если они не перекрыты, (если отсутствует их новая версия. Переменная char *name; объявлена скрытой (private) в классе Man. Особенность этого типа переменных заключается в том, что, несмотря на факт их наследования производными классами (см. таблицу размеров объектов), доступ к ним возможен только с помощью методов, также унаследованных от базового класса. Так, если вставить строку name="Alex"; внутрь собственной функции производного класса (например, Out), то компилятор выдаст сообщение об ошибке, так как переменная name не доступна в функции Stud::Out, несмотря на факт принадлежности ее классу Stud. Однако, внутри Man::Out private-переменная name (полученная от класса Man) доступна, что видно из примера. Анализ используемых конструкций позволяет убедиться, что в С++ сохраняется традиция предельного лаконизма форм и описаний, характерная для языка С. В то же время конструкции языка обладают поразительной гибкостью. Обратите внимание, как просто метод Out подстраивается в производном классе. В результате одно и то же послание, передаваемое объектам разных классов, порождает различные цепочки действий.

С++ уважает старших

Все три класса в примере имеют свои конструкторы, но для инициализации наследованных данных используется конструктор базового класса. При таком механизме передачи параметров конструкторы вызываются по очереди. Например, при создании объекта класса Stud сначала запустится конструктор Man, затем выполнится тело конструктора Stud. Запомнить порядок вызова помогает правило «С++ уважает старших». Обращение к конструктору базового класса происходит благодаря явному указанию в заголовке конструктора производного класса.

Только два класса из трех (Man и Prof) имеют деструкторы. Вы помните, что деструктор необходим классу, если при выходе программы из области действия объекта в области heap может остаться память, ранее отведенная для каких-то из переменных объекта. Деструктор, обычно, освобождает эту память. В нашенм случае система сама может справиться с этой проблемой, так при выходе из main завершается весь процесс, но с целью иллюстрации мы создали деструкторы и вставили в них диагностический вывод. При завершении программы вызов деструкторов происходит в порядке, обратном вызову конструкторов.

Сначала будет вызван деструктор для объекта prof класса Prof, который освободит память, занятую строкой текста dept. Он же автоматически вызовет деструктор родительского класса Man, который освободит память, занятую массивом символов, на который указывает переменная name все того же объекта prof. Будет ли вызван деструктор для объекта stud? Так как мы его не объявили, то будет вызван деструктор, предоставленный объекту компилятором. Он лишь вызовет родительскую версию, которая освободит память, где хранится имя студента. Последним сработает деструктор для объекта man класса Man, который освободит память, захваченную для имени объекта man.

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

class Parent       // Базовый класс

{

int p;            // Его переменная

public:

Parent (int q) { p=q; } // Конструктор

};

class Derived : public Parent   // Производный класс

{

int d;            // Его переменная

Parent par;       // Вложенный объект базового класса

public:

Derived (int a, int b, int c) : Parent (a), par(b)

{

  d=c;

}

}

Конструктор класса Derived имеет три формальных параметра, которые распределяются между конструкторами и присваиваются:

¨  a — переменной Derived::p, наследованной от Parent;

¨  b — переменной Derived::par.p;

¨  c — переменной d класса Derived.

Выражение во второй строке перечисления может быть прочитано так: переменная p объекта par класса Derived. Конструкторы активизируются в следующем порядке: сначала базового класса, затем классов, которым принадлежат внутренние переменные и, наконец, своего (производного) класса. Мнемоническим правилом будет: «С++ уважает старших и гостей».

Управление механизмом наследования

Достаточно редко используется, но существует в языке возможность управлять атрибутами доступа унаследованных данных и методов. Так, класс Stud мог быть объявлен с описателем protected:

class Stud : protected Man { ... }

В этом случае унаследованные из класса Man public-методы перешли бы (в классе Stud) в секцию protected. Все полученные по наследству private- и protected-данные остались бы в этих же секциях. Отметьте, что еще реже (если вообще) можно встретить private-тип наследования. Он переводит все наследованное имущество в разряд private. Чтобы работать с объектами такого класса, необходимо, чтобы класс имел достаточное количество своих данных и методов, доступных извне.

Вопросы и упражнения