Добавил:
Опубликованный материал нарушает ваши авторские права? Сообщите нам.
Вуз: Предмет: Файл:
Язык программирования С++ и его «подводные камни».DOC
Скачиваний:
44
Добавлен:
01.05.2014
Размер:
1.02 Mб
Скачать

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

Ключевым моментом объектно-ориентированного программи­рования, нашедшим свое воплощение в С++, является возможность созда­ния иерархической структуры классов, которая базируется на наследова­нии – некоторый класс может передавать свои компоненты другому классу. При этом первый класс называется базовым, а второй – производным клас­сом (или подклассом). Имя базового класса указывается непосредственно после имени производного, до открывающей фигурной скобки:

class BaseClass

{

// Компоненты базового класса

};

class SuibClass: public BaseClass

{

// Компоненты производного класса

};

В данном случае производный класс SubClass наследует все компоненты базового, а также содержит любые, определенные непосредственно в нем самом, компоненты.

Обратите внимание на ключевое слово public перед именем базового клас­са, которое определяет тип наследования – в данном случае общедоступ­ный. Можно также задавать частный (private) тип наследования. Разные типы влияют на доступность компонентов базового класса в производном. Чтобы разобраться в этом вопросе, рассмотрим пример.

Пример 3.1

class BaseClass

{

public:

// Общедоступные компоненты базового класса

int nPub1Base;

void funcBase( );

protected:

// Защищённые компоненты базового класса

int nProtBase;

private:

// Частные компоненты базового класса

int nPrivBase;

};

// class SubClass: public BaseClass //public-наследование

class SubClass: private BaseClass //private-наследование

{

public:

// Общедоступные компоненты производного класса

int nPublSub;

void funcSub( );

protected:

// Защищённые компоненты производного класса

int nProtSub;

private:

// Частные компоненты производного класса

int nPrivSub;

};

void main (int argc, char *argv[])

{

BaseClass clBase;

SubClass clSub;

clBase,nPublBase = 7;

clBase.funcBase( );

clSub.nPublBase = 7; //ОШИБКА! -недоступно

clSub.funcBase( ); //ОШИБКА! -недоступно

clSub.nPublSub = 77;

clSub.funcSub( );

}

void BaseClass :: funcBase( )

{

//Имеется доступ к компонентам своего класса BaseClass

nPublBase = 1;

nProtBase = 2;

nPrivBase = 3;

}

void SubClass :: funcSub( )

{

//Имеется доступ только к public и protected компонентам базового класса, которые для

//производного класса являются private компонентами, доступными только из его методов.

nPublBase = 1;

funcBase( );

nProtBase = 2;

nPrivBase = 3; //ОШИБКА! - недоступно

//Имеется доступ к любым компонентам своего класса

nPublSub = 4;

nProtSub = 5;

nPrivSub = 6;

}

Что интересного в этом примере? Прежде всего обратите внимание, что компонент базового класса, наследуемый как public, сохраняет тот же тип доступа.

Несколько иначе дело обстоит при типе наследования private. Как вы, ко­нечно же, помните, частный компонент класса доступен только другим компонентам и друзьям класса. Поэтому вполне логично, что такие частные компоненты недоступны в производном классе. Ведь в противном случае программист имел бы возможность получить доступ к таким компонентам, реализовав в производном классе необходимые методы, а это противоречит самой идеологии инкапсуляции.

Однако часто необходимо из производного класса обеспечить доступ к некоторым компонентам базового. Чтобы не пришлось объявлять такие компоненты общедоступными (public), т. е. полностью «раскрывать» их, в языке С++ и введена специальная категория доступности – защищенная (protected). Компоненты, которые имеют такой уровень доступа, заблоки­рованы для всех частей программы, за исключением компонентов самого класса и производного от него.

Осталось только привести сводку правил наследования доступа (табл. 3.1).

Таблица 3.1. Правила наследования доступа

Тип наследования доступа

Доступность в базовом классе

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

public

public

public

protected

protected

private

private

private

public

private

protected

private

private

недоступны

По умолчанию установлен тип наследования доступа private.

Чтобы закончить с вопросом правил наследования доступа, осталось ска­зать, что в производном классе можно изменять доступность отдельных компонентов базового класса. Например, если в объявление производногокласса BaseClass из предыдущего примера внести следующее дополнение:

class SubClass: BaseClass // private-наследованиепоумолчанию

{

public:

// Общедоступные компоненты производного класса

BaseClass::nPublBase;; // Конкретно объявляем как public

int nPublSub;

void funcSub( );

. . .

};

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

clSub.nPublBase = 7;

Производный класс сам может служить базовым, т. е. от него можно обра­зовывать новые классы. В этом случае полученный класс включит в себя элементы всех его базовых классов. От одного базового класса можно обра­зовывать несколько производных. Таким образом, возможна древовидная структура иерархии классов (рис. 3.1).

Схема размещения в памяти объекта простого производного класса пред­ставлена на рис. 3.2.

Во всех рассмотренных примерах производный класс был образован от од­ною базового класса – так называемое единичное наследование. Однако ме­ханизм наследования в С++ более развит и включает наряду с единичным имножественное наследование, при котором производный класс образуется сразу от нескольких базовых, что позволяет создавать достаточно сложные иерархии классов (рис. 3.3).

Пример 3.2

class Base1

{

// Компоненты первого базового класса

};

class Base2

{

// Компоненты второго базового класса

};

class Derived : public Base1, private Base2

{

// Компоненты производного класса

};

Рис. 3.1. Древовидная иерархия классов

Рис. 3.2. Схема размещения в памяти объекта простого производного класса

Рис. 3.3. Сетевая иерархия классов

В объявлении класса может быть записано любое количество базовых клас­сов, перечисленных через запятую. Порядок, в котором перечисляются ба­зовые классы, произвольный. Он влияет только на последовательность вы­зова конструкторов, которая является следующей:

Сначала вызываются конструкторы всех виртуальных базовых классов; если их несколько, то конструкторы вызываются в порядке их наследо­вания (виртуальные классы мы рассмотрим ниже).

Затем конструкторы не виртуальных базовых классов в порядке их насле­дования.

И, наконец, конструкторы всех компонентных классов.

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

Поскольку множественное наследование является более сложной конструк­цией по сравнению с единичным, то вполне очевидно, что при этом возни­кают и некоторые проблемы.

В случае множественного наследования вполне вероятно, что один и тот же класс может унаследовать несколько объектов с одним и тем же именем:

class Base1

{

int nObj;

// Остальные компоненты первого базового класса

};

class Base2

{

int nObj;

// Остальные компоненты второго базового класса

};

class Derived: public Base1, public Base2

{

// Компоненты производного базового класса

} clDer;

В данном случае мы имеем два объекта clDer.nObj с одним и тем же име­нем, а значит, обращаться к ним просто по имени нельзя. В этом случае выход заключается в использовании оператора разрешения контекста и полных имен объектов:

clDer.BaseClass1::nObj++;

clDer.BaseClass1::nObj++;

Такое же решение проблемы необходимо использовать в случае наследова­ния через промежуточный класс:

Пример 3.3

class BaseClass

{

int nBaseObj;

};

class FirstBaseClass: public BaseClass

{

int nFirstObj;

};

class SecondBaseClass: public BaseClass

{

int nSecondObj;

};

class DerivedClass: public FirstBaseClass, public SecondBaseClass

{

long nDerivedObj;

// Остальные компоненты производного класса

} clDer;

. . .

clDer.FirstBaseClass::nObj++;

clDer.SecondBaseClass::nObj++;

При использовании наследования через промежуточный класс возникает другая проблема – при приведении типа указателя одного класса к другому может возникнуть неоднозначность:

DerivedClass clDerived;

DerivedClass *ptrDer = &clDerived;

BaseClass *ptrBase;

ptrBase = (Base *) ptrDer; // ОШИБКА! – неоднозначность приведения типа

Проблема решается выполнением явного приведения типа к классу, для ко­торого неоднозначностей не возникает

ptrBase = (Base*) (FirstBase*)ptrDer; // Теперь–правильно

Размещение объектов производных классов в памяти в случае простого множественного наследования (пример 3.2) представлено на рис. 3.4.

Рис. 3.4. Размещение объекта в памяти при простом множественном наследовании

Обратите внимание, что если мы передадим Derived* функции, ожидающей Base1, то всё будет в порядке – неоднозначностей не возникает. Однако при вызове функции, ожидающей поступления Base2, возникнет проблема, и необходимо выполнять явное приведение типа.

Посмотрим, что же получается в случае наследования через промежуточный класс, описанный в примере 3.3. Проблема, которая возникает в этом случае, наглядно продемонстрирована на рис. 3.5 – наличие дублирования компонентов «самого» базового класса BaseClass.

Рис. 3.5. Схема размещения объекта при наследовании через промежуточный класс

Некоторые программисты могут и не посчитать это проблемой и сознатель­но использовать обе (или больше) копии. Однако в большинстве случаев такое положение все-таки нежелательно. Чтобы в таких случаях разрешить наследование только одной копии BaseClass, в язык С++ включено ключевое слово virtual, с помощью которого реализуется так называемое вирту­альное наследование:

class BaseClass

{

int nBaseObj;

};

class FirstBaseClass: virtual public BaseClass

{

int nFirstObj;

};

class SecondBaseClass: virtual public BaseClass

{

int nSecondObj;

};

class DerivedClass: public FirstBaseClass, public SecondBaseClass

{

long nDerivedObj;

// Остальные компоненты производного класса

} clDer;

Такое дополнение (ключевым словом virtual) изменяет схему размещения объекта производного класса DerivedClass, как это показано на рис. 3.6.

Теперь существует только одна копия BaseClass, и нет никакой неодно­значности и избыточности.

Рис. 3.6. Схема размещения объекта при виртуальном множественном наследовании