Синтаксис наследования задаётся необязательным элементом заголовка класса, который называется спецификацией базы и описывается следующим множеством форм Бэкуса-Наура:
СпецификацияБазы ::= : СписокБаз СписокБаз ::= [СписокБаз,] ОписательБазы ОписательБазы ::= ПолноеИмяКласса ::= [virtual] [СпецификаторДоступа] ПолноеИмяКласса ::= [СпецификаторДоступа] [virtual] ПолноеИмяКласса
Нам ещё предстоит выяснить назначение элементов описателя базы, но уже очевидно, что спецификация базы представляет собой список имён классов. Поскольку производный класс наследует данные и функции базового класса, базовые классы обязательно должны быть объявлены до объявления производного класса.
Для начала рассмотрим пример объявления нескольких классов. В этом примере задаются отношения наследования между тремя классами (классы A, B, C). При этом C наследует свойства класса B, который, в свою очередь, является наследником класса A. В этом примере все члены классов объявляются со спецификатором public, к которому мы пока относимся (пока!) как к должному. В этих классах мы объявим (просто обозначим) самые простые варианты конструкторов и деструкторов. В настоящий момент нам важно исключительно их существование.
#include <iostream.h> class A { public: A(){}; ~A(){}; int x0; int f0 () {return 1;}; }; class B : public A { public: B(){}; ~B(){}; int x1; int x2; int xx; int f1 () {return 100;}; int f2 () {return 200;}; }; class C : public B { public: C(){}; ~C(){}; int x1; int x2; int x3; int f1 () {return 1000;}; int f3 () {return 3000;}; }; void main () {C MyObject;}
Перед нами пример простого наследования. Каждый производный класс при объявлении наследует свойства лишь одного базового класса. В качестве базового класса можно использовать лишь полностью объявленные классы. Неполного предварительного объявления здесь недостаточно. Для наглядного представления структуры производных классов используются так называемые направленные ациклические графы. Узлы этого графа представляют классы, дуги - отношение наследования.
Вот как выглядит направленный ациклический граф ранее приведённого в качестве примера производного класса C:
A B C
Структуру производного класса можно также представить в виде таблицы (или схемы класса), отображающей общее устройство класса:
A B C
В C++ различаются непосредственные и косвенные базовые классы. Непосредственный базовый класс упоминается в списке баз производного класса. Косвенным базовым классом для производного класса считается класс, который является базовым классом для одного из классов, упомянутых в списке баз данного производного класса.
В нашем примере для класса C непосредственным базовым классом является B, косвенным - A. Следует иметь в виду, что порядок "сцепления" классов, образующих производный класс, зависит от реализации, а потому все схемы классов и объектов имеют характер имеют чисто иллюстративный характер.
Дополним нашу схему, включив в неё объявления всех членов классов, включая, конструкторы и деструкторы.
В результате мы получаем полную схему производного класса со всеми его компонентами, вместе с его непосредственными базовыми классами, а также и косвенными базовыми классами.
A A(); ~A(); int x0; int f0 (); B B(); ~B(); int x1; int x2; int xx; int f1(); int f2(); C C(); ~C(); int x1; int x2; int xx; int f1(); int f2();
Это схема класса, а не объекта. Образно говоря, наша схема подобна схеме многоэтажного бункера, разделённого на три уровня. На схеме эти уровни разделяются двойными линиями. Класс C занимает самый нижний уровень. Именно этот класс имеет неограниченные (по крайней мере, в нашей версии объявления производного класса) возможности и полномочия доступа к элементам базовых классов. Именно с нижнего уровня можно изменять все (опять же, в нашей версии объявления класса) значения данных-членов класса и вызывать все (или почти все) функции-члены класса.
Объект-представитель класса C является единым блоком объектов и включает собственные данные-члены класса C, а также данные-члены классов B и A. Как известно, функции-члены классов, конструкторы и деструкторы не включаются в состав объекта и располагаются в памяти отдельно от объектов. Так что схему объекта-представителя класса можно представить, буквально удалив из схемы класса функции-члены, конструкторы и деструкторы.
Следует также иметь в виду, что на схеме класса располагаются лишь объявления данных-членов, тогда как схема объекта содержит обозначения определённых областей памяти, представляющих данные-члены конкретного объекта.
Итак, выполнение оператора определения
C MyObj;
приводит к появлению в памяти объекта под именем MyObj. Рассмотрим схему этого объекта. Её отличие от схемы класса очевидно. Здесь мы будем использовать уже известный нам метасимвол ::= (состоит из). На схеме объекта информация о типе данного-члена будет заключаться в круглые скобки.
MyObj::= A (int)x0 B (int)x1 (int)x2 (int)xx C (int)x1 (int)x2 (int)xx
Перед нами объект сложной структуры, в буквальном смысле собранный на основе нескольких классов. В его создании принимали участие несколько конструкторов. Порядок их вызова строго регламентирован. Вначале вызываются конструкторы базовых классов. Следом вызываются конструкторы производных классов.
Благодаря реализации принципа наследования, объект представляет собой цельное сооружение. Из объекта можно вызвать функции-члены базовых объектов. Эти функции наследуются производным классом от своих прямых и косвенных базовых классов. Непосредственно от объекта возможен доступ ко всем данным-членам. Данные-члены базовых классов также наследуются производными классами.
Если переопределить деструкторы базовых и производных классов таким образом, чтобы они сообщали о начале своего выполнения, то за вызовом деструктора производного класса C непосредственно из объекта MyObj:
MyObj.~C();
последует серия сообщений о выполнении деструкторов базовых классов. Разрушение производного объекта сопровождается разрушением его базовых компонентов. Причём порядок вызова деструкторов противоположен порядку вызова конструкторов.
А вот вызвать деструктор базового класса из объекта производного класса невозможно:
MyObj.~B(); // Так нельзя. Это ошибка!
Частичное разрушение объекта в C++ не допускается. БАЗОВЫЕ ДЕСТРУКТОРЫ НЕ НАСЛЕДУЮТСЯ. Таков один из принципов наследования.
Если бы можно было вызывать конструктор непосредственно из объекта, аналогичное утверждение о наследовании можно было бы сделать и по поводу конструкторов.
Однако утверждение о том, что базовый конструктор не наследуется так же корректно, как и утверждение о том, что стиральная машина не выполняет фигуры высшего пилотажа. Стиральная машина в принципе не летает. НИ ОДИН КОНСТРУКТОР (ДАЖЕ КОНСТРУКТОР ПРОИЗВОДНОГО КЛАССА) НЕ ВЫЗЫВАЕТСЯ ИЗ ОБЪЕКТА.
К моменту начала разбора структуры производного класса, транслятору становятся известны основные характеристики базовых классов. Базовые классы включаются в состав производных классов в качестве составных элементов. Это означает, что в производном классе (в его функциях) можно обращаться к данным-членам и вызывать функции-члены базовых классов. Можно, если только этому ничего не мешает (о том, что может этому помешать - немного позже).
Как раз в нашем случае в этом смысле всё в порядке, и мы приступаем к модификации исходного кода нашей программы.
Прежде всего, изменим код функции с именем f1, объявленной в классе C. Мы оставим в классе лишь её объявление, а саму функцию определим вне класса, воспользовавшись при этом её квалифицированным именем.
Проблемы, связанные с одноименными членами класса решаются с помощью операции разрешения области видимости. Впрочем, нам это давно известно:
int C ::f1() { A::f0(); /*Вызов функции-члена класса A.*/ f0(); /* Для вызова этой функции можно не использовать специфицированного имени. Функция под именем f0 одна на все классы. И транслятор безошибочно определяет её принадлежность. */ A::x0 = 1; B::x0 = 2; C::x0 = 3; x0 = 4; /* К моменту разбора этой функции-члена, транслятору известна структура всех составных классов. Переменная x0 (как и функция f0) обладает уникальным именем и является общим достоянием базовых и производных классов. При обращении к ней может быть использовано как её собственное имя, так и имя с любой квалификацией. Аналогичным образом может быть также вызвана и функция f0(). */ B::f0(); C::f0(); /* Изменение значений данных-членов. */ //A::x1 = 1; /* Ошибка! Переменная x1 в классе A не объявлялась.*/ B::x1 = 2; C::x1 = 3; x1 = 4; /* Переменная x1 объявляется в двух классах. Транслятор определяет принадлежность данных-членов по квалифицированным именам. В последнем операторе присвоения транслятор считает переменную x1 собственностью класса C, поскольку этот оператор располагается "на территории" этого класса. Если бы класс C не содержал объявления переменной x1, последние три оператора были бы соотнесены с классом B. */ //A::xx = 1; /* Ошибка! Переменная xx в классе A не объявлялась.*/ B::xx = 2; C::xx = 3; xx = 4; /* Аналогичным образом обстоят дела с переменной xx, объявленной в классе B. Хотя xx не объявлялась в классе C, транслятор рассматривает эту переменную как элемент этого класса и не возражает против квалифицированного имени C::xx. В последнем операторе транслятор рассматривает переменную xx как член класса B. */ return 150; } Теперь переопределим функцию-член класса B. При её разборе (даже если определение этой функции располагается после объявления класса C), транслятор воспринимает лишь имена базовых классов. В это время транслятор забывает о существовании класса C. А потому упоминание этого имени воспринимается им как ошибка. int B ::f1() { A::f0(); A::x0 = 1; B::x0 = 2; //C::x0 = 3; /* Ошибка. */ x0 = 4; B::f0(); //C::f0(); /* Ошибка. */ /* Изменение значений данных-членов. */ //A::x1 = 1; /* Ошибка. Переменная x1 в классе A не объявлялась.*/ B::x1 = 2; //C::x1 = 3; /* Ошибка. */ x1 = 4; //A::xx = 1; /* Ошибка! Переменная xx в классе A не объявлялась.*/ B::xx = 2; //C::xx = 3; /* Ошибка. */ xx = 4; return 100; }
Нам остаётся рассмотреть, каким образом транслятор соотносит члены класса непосредственно в объекте. Для этого переопределим функцию main():
void main () { C MyObj; MyObj.x0 = 0; MyObj.B::x0 = 1; MyObj.C::x0 = 2; MyObj.f0(); MyObj.A::f0(); MyObj.C::f0(); /* Поиск "снизу-вверх" является для транслятора обычным делом. Транслятор способен отыскать нужные функции и данные даже у косвенного базового класса. Главное, чтобы они были там объявлены. И при было бы возможным однозначное соотнесение класса и его члена. */ MyObj.x1 = 777; MyObj.B::x1 = 999; cout << MyObj.A::x1 << "-" << MyObj.B::x1; /* Процесс соотнесения осуществляется от потомков к предкам. Не специфицированный член класса x1 считается членом "ближайшего" производного класса, о чём и свидетельствует последняя тройка операторов. */ MyObj.B::f2(); MyObj.C::f2(); /* И опять успешное соотнесение благодаря поиску "снизу-вверх". Недостающие элементы в производном классе можно поискать по базовым классам. Важно, чтобы они там были. */ // MyObj.A::f1(); // MyObj.A::f2(); // MyObj.A::f3(); // MyObj.B::f3(); /* А вот "сверху вниз" транслятор смотреть не может. Предки не отвечают за потомков. */ }
Таким образом, корректное обращение к членам класса в программе обеспечивается операцией разрешения области видимости. Квалифицированное имя задаёт область действия имени (класс), в котором начинается (!) поиск данного члена класса. Принципы поиска понятны из ранее приведённого примера.
Назад | Содержание | Вперед