Добавил:
Upload Опубликованный материал нарушает ваши авторские права? Сообщите нам.
Вуз: Предмет: Файл:
Скачиваний:
47
Добавлен:
16.02.2016
Размер:
455.17 Кб
Скачать

Глава 13

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

Простое наследование

Язык C++ позволяет классу наследовать данные-члены и функции-члены одного или нескольких других классов. При этом новый класс называют производным классом (или классом-потомком). Класс, элементы которого наследуются, называется базовым классом (родительским классом или классом-предком) для своего производного класса. Наследование дает возможность некоторые общие черты поведения классов абстрагировать в од­ном базовом классе. Производные классы, наследуя это общее поведение, могут его несколько видоизменять, переопределяя некоторые функции-члены базового класса, или дополнять, вводя новые данные-члены и функции-члены. Таким образом, опреде­ление производного класса значительно сокращается, поскольку нужно определить только отличающие его от производных клас­сов черты поведения.

Синтаксис объявления производного класса имеет следующий вид:

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

class Basel

{

//...

};

//Базовой класс

class Base2

{

//...

};

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

class BaseN

{

//...

};

//Производный класс

class Derived : <спецификатор_доступа> Basel,

<спецификатор_доступа> Base2,

…,

<спецификатор_доступа> BaseN

{

//...

};

Здесь <спецификатор_доступа> — это public, protected или private; он не является обязательным и по умолчанию принимает значение private для классов и public для структур. Спецификатор доступа при наследовании опреде­ляет уровень доступа к элементам базового класса, который по­лучают элементы производного класса. В приведенной ниже таб­лице описаны возможные варианты наследуемого доступа.

Таблица 13.1.

Наследуемый доступ

Спецификатор наследуемого доступа

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

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

public

Public

Protected

Private

Public

Protected

Недоступны

protected

Public

Protected

Private

Protected

Protected

Недоступны

private

Public

Protected

Private

Private

Private

Недоступны

Изучение таблицы показывает, что спецификатор наследуемо­го доступа устанавливает тот уровень доступа, до которого по­нижается уровень доступа к членам, установленный в базовом классе. Из этого правила можно сделать исключения. Если спе­цификатор наследуемого доступа установлен как private, то public-члены базового класса будут являться private-членами в производном классе. Однако можно сделать некоторые из членов базового класса открытыми в производном классе, объ­явив их в секции public производного класса. Например,

class Base

{

int х,у;

public:

int GetX(){return x;}

int GetY(){return у;}

};

class Derived : private Base

{

public:

Base::GetX;

};

main()

{

int x;

Derived ob;

x = ob.GetX() ;

return 0;

}

Таким образом, при таком наследовании открытые и защи­щенные члены базового класса будут доступны только для чле­нов данного производного класса, и все члены базового класса, кроме явно объявленных в разделе public или protected, будут закрытыми для следующих производных классов. Этот прием позволяет отсечь доступ к членам базового класса при по­строении иерархии классов с отношением родитель — потомок.

Напомним, что при любом способе наследования в производ­ном классе доступны только открытые (public-) и защищенные (protected-) члены базового класса (хотя наследуются все члены базового класса). Иначе говоря, закрытые члены базового класса остаются закрытыми, независимо от того, как этот класс наследуется.

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

• конструкторы;

• конструкторы копирования;

• деструкторы;

• операторы присвоения, определенные программистом;

• друзья класса.

Здесь упомянут еще не рассмотренный нами вид членов клас­са. Он будет рассмотрен позже.

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

#include <iostream.h>

class Coord

{

int x, у;

public:

Coord (int _x, int _y) {x = _x; у = _y;}

Coord(){x = 0; у = 0;}

int GetX(){return x;}

int GetY(){return y;}

void SetX(int _x){x = _x;}

void SetY(int _y){y = _y;}

};

class OutCoord: public Coord

{

public:

OutCoord(int _x, int _y): Coord(_x, _y){}

void ShowX(){cout « GetX() « ' ' ;}

void ShowY(){cout « GetY()« ' ';}

};

main ()

{

OutCoord* ptr;

ptr = new OutCoord(10,20); //Создание объекта a кучe

ptr->ShowX();

ptr->ShowY();

cout « "\n";

delete ptr; //Удаление объекта из кучи

return 0;

}

Здесь класс OutCoord является производным от базового класса Coord. К членам класса Coord он добавляет две откры­тые функции и конструктор. Напомним, что конструкторы не наследуются. Поэтому производный класс либо должен объявить свой конструктор, либо предоставить возможность компилятору сгенерировать конструктор по умолчанию. Правила, действую­щие в отношении конструкторов по умолчанию, были нами из­ложены выше. Рассмотрим теперь, как строится конструктор производного класса, предоставляемый программистом. По­скольку производный класс должен унаследовать все члены ро­дительского, при построении объекта своего класса он должен обеспечить инициализацию унаследованных данных-членов, причем она должна быть выполнена до инициализации данных-членов производного класса, так как последние могут использо­вать значения первых. В связи с этим для построения конструк­тора производного класса применяется следующая конструкция:

<констр_произв_класса> (<список параметров>):

<констр_базового_класса> (<cписок_apг>)

{<тело_конструктора>)

То есть используется список инициализации элементов, в ко­тором указывается конструктор базового класса. Часть парамет­ров, переданных конструктору производного класса, обычно ис­пользуется в качестве аргументов конструктора базового класса. Затем в теле конструктора производного класса выполняется инициализация данных-членов, принадлежащих собственно это­му классу.

В приведенном выше примере эта конструкция использована для создания конструктора класса Outclass. В данном случае конструктор имеет вид:

OutCoord(int _x, int _y): Coord(_x, _y){}

Тело конструктора оставлено пустым, так как класс OutCoord не имеет собственных данных-членов. Все параметры конструктора производного класса просто переданы конструкто­ру базового класса для осуществления инициализации. Аргумен­ты конструктору производного класса передаются в операторе new, который неявно вызывает этот конструктор. Затем про­грамма вызывает функции-члены ShowX () и ShowY () объекта, являющегося представителем производного класса, которые вы­водят на экран в одной строке заданные значения координат. Наконец, оператор delete вызывает удаление объекта, для чего он неявно вызывает деструктор производного класса.

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

Хотя конструктор базового класса и наследуется, вызывается он только компилятором, когда конструируется объект производ­ного класса. Конструктор, в отличие от других унаследованных функций, вызвать явно нельзя.

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

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

#include <iostream.h>

class Base

{

public:

Base()

{

cout « "Мы в конструкторе "« "базового класса" « "\n";

}

~Base ()

{

cout « "Мы в деструкторе базового” « "класса" « "\n";

}

};

class Derived: public Base

{

public:

Derived()

{

cout « "Мы в конструкторе производного класса" « "\n";

}

~Derived ()

{

cout «"Мы в деструкторе производного класса" « "\n";

}

};

main()

{

Derived ob;

return 0;

}

Эта программа выводит на экран следующее:

Мы в конструкторе базового класса

Мы в конструкторе производного класса

Мы в деструкторе производного класса

Мы в деструкторе базового класса

Для дальнейшего очень важно понимать работу конструкто­ров и деструкторов при наследовании.

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

Модифицируем рассмотренный нами пример так, чтобы про­изводный класс переопределял функции-члены своего базового класса GetX() nGetY():

#include <iostream.h>

class Coord

{

protected:

int x, y;

public:

Coord(int _x, int _y) {x = _x; у = _y;}

Coord() {x = 0; у = 0;}

int GetX() {return x;}

int GetY(){return y;}

void SetX(int _x){x = _x;}

void SetY(int _y){y = _y;}

};

class OutCoord: public Coord

{

public:

OutCoord(int _x, int _y): Coord(_x,_y){}

int GetX(){return ++x;}

int GetY(){return ++y;}

void ShowX(){cout « GetX() « ' ';}

void ShowY(){cout « GetY()« ' ';}

};

main()

{

OutCoord* ptr;

ptr = new OutCoord(10,20); //Создание объекта в куче

ptr->ShowX() ;

ptr->ShowY();

cout « "\n";

delete ptr; //Удаление объекта из кучи

return 0;

}

В этом случае функции-члены ShowY () и ShowX () объекта ptr используют для получения значений данных-членов переоп­ределенные варианты функций базового класса GetX () и GetY (). В результате на экран программа выведет инкрементированные значения заданных координат.

Иногда возникает необходимость вызвать функцию-член ба­зового класса, а не ее переопределенный вариант, это можно сде­лать с помощью операции разрешения области видимости, при­меняемой в форме:

<имя_класса>: :<имя_члена>

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

int

OutCoord::GetX()

{

int z;

//Вызов переопределенной функции

z = Coord::GetX();

return ++z;

}

Точно так же можно было бы использовать операцию разре­шения области видимости для квалификации вызываемой пере­груженной функции в ShowX ():

void ShowX(){cout « Coord::GetX() « ' ';}

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

Множественное наследование

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

Следующий пример демонстрирует множественное наследование.

#include <iostream.h>

#include <conio.h>

#include <string.h>

class Coord

{

int x, y;

public:

Coord(int _x, int _y){x = _x; у = _y;}

int GetX() {return x;}

int GetY() {return y;}

void SetX(int _x){x = _x;}

void SetY(int _y){y = _y;}

};

class SaveMsg

{

char Message[80];

public:

SaveMsg(char* msg){SetMsg(msg);}

void SetMsg(char* msg) {strcpy(Message, msg);}

void ShowMsg() {cout « Message;}

};

class PrintMsg: public Coord, public SaveMsg

{

public:

PrintMsg(int _x, int _y, char* msg): Coord(_x, _y), SaveMsg(msg){}

void Show();

};

void

PrintMsg::Show()

{

gotoxy(GetX() , GetY());

ShowMsg();

}

main()

{

PrintMsg * ptr;

ptr =new PrintMsg(10, 5, "Множественное ");

ptr->Show() ;

ptr->SetX(25);

ptr->SetY(5);

ptr->SetMsg("наследование");

ptr->Show();

delete ptr;

return 0;

}

В этом примере класс Coord отвечает за хранение и установ­ку значений координат на экране, класс SaveMsg хранит и уста­навливает сообщение, а класс PrintMsg, являющийся произ­водным от них, выводит это сообщение на экран в заданных ко­ординатах. Этот пример демонстрирует также, как осуществля­ется передача параметров конструкторам базовых классов. При множественном наследовании, как и при простом, конструкторы базовых классов вызываются компилятором до вызова конструк­тора производного класса. Единственная возможность передать им аргументы — использовать список инициализации элементов. Причем порядок объявления базовых классов при наследовании определяет и порядок вызова их конструкторов, которому должен соответствовать порядок следования конструкторов базовых классов в списке инициализации. В данном случае класс PrintMsg содержит такое объявление о наследовании:

class PrintMsg: public Coord, public SaveMsg

Этому объявлению соответствует следующий конструктор:

PrintMsg(int _x, int _y, char* msg): Coord(_x, _y), SaveMsg(msg){}

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

ptr = new PrintMsg(10, 5, "Множественное ");

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

Следующие за этим вызовы функций-членов SetX (), SetY () и SetMsg () приводят к установке новых значений ко­ординат и нового сообщения, которое выводится повторным вы­зовом функции Show ().

Рассмотрим еще один пример, который демонстрирует поря­док вызова конструкторов и деструкторов базовых классов.

#include <iostream.h>

class Basel

{

public:

Basel(){cout «"Мы в конструкторе Basel\n";}

~Basel(){cout «"Мы в деструкторе Basel\n";}

};

class Base2

{

public:

Base2() {cout «"Мы в конструкторе Base2\n";}

~Base2(){cout «"Мы в деструкторе Base2\n";}

};

class Derived: public Basel, public Base2

{

public:

Derived()

{

cout « "Мы в конструкторе Derived \n";

}

~Derived()

{

cout « "Мы в деструкторе Derived \n";

}

};

main()

{

Derived ob;.

return 0;

}

Эта программа выводит на экран следующее:

Мы в конструкторе Basel

Мы в конструкторе Base2

Мы в конструкторе Derived

Мы в деструкторе Derived

Мы в деструкторе Base2

Мы в деструкторе Basel

Этот пример наглядно демонстрирует, что конструкторы ба­зовых классов вызываются в порядке их объявления. Деструкторы вызываются в обратном порядке.

Виртуальные базовые классы

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

class IndBase

{

private:

int x;

public:

int GetX(){return x;}

void SetX(int _x){x = _x;}

double var;

};

class Basel: public IndBase

{

//...

};

class Base2: public IndBase

{

//...

};

class Derived: public Basel, public Base2

{

//...

};

main()

{

Derived ob;

ob.var = 5.0;

ob.SetX(0);

int z = ob. GetX () ;

//ob.Basel::var = 5.0;

//ob.Basel::SetX(0) ;

//int z = ob.Basel::GetX ();

return 0;

}

Здесь класс Derived косвенно наследует класс IndBase через свои базовые классы Basel и Base2. Поэтому при ком­пиляции приведенного примера возникнут ошибки, вызванные неоднозначностью обращения к членам класса var и GetX () в строках:

ob.var =5.0;

ob.SetX(0);

int z = ob.GetX() ;

Чтобы избежать этой неоднозначности, можно использовать квалификацию имен, применив операцию разрешения видимости:

ob.Basel::var = 5.0;

ob.Basel::SetX(0);

int z = ob.Basel::GetX();

Можно также квалифицировать эти вызовы следующим образом:

ob.Base2::var = 5.0;

ob.Base2::SetX(0);

int z = ob.Base2::GetX();

Хотя этот способ и позволяет избежать неоднозначности при вызове, тем не менее класс IndBase будет включен в состав класса Derived дважды, увеличивая его размер. Избежать по­вторного включения косвенного базового класса в производный класс можно, дав указание компилятору использовать виртуаль­ный базовый класс. Это осуществляется с помощью ключевого слова virtual, которое указывается перед спецификатором на­следуемого доступа или после него. Следующий пример является модифицированным вариантом предыдущего, использующим класс IndBase в качестве виртуального базового класса.

class IndBase

{

private:

int x;

public:

int GetX(){return x;}

void SetX(int _x){x = _x;} double var;

};

class Basel: virtual public IndBase

{

//...

};

class Base2: virtual public IndBase

{

//...

};

class Derived: public Basel, public Base2

{

//...

};

main()

{

Derived ob;

ob.var = 5.0;

ob.SetX(0);

int z = ob.GetX() ;

return 0;

}

В этом случае класс Derived содержит один экземпляр клас­са IndBase. и вызовы

ob.var = 5.0;

ob.SetX(0);

int z = ob.GetX() ;

не приводят к появлению сообщений компилятора об ошибках, связанных с неоднозначностью.

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

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

class Basel{};

class Base2{};

class Derived: public Basel, public Base2 {};

main()

{

Derived ob;

//...

return 0;

}

конструкторы вызываются в следующем порядке:

Basel () ;

Base2() ;

Derived () ;

Объявление базового класса виртуальным изменяет порядок вызова конструкторов при создании экземпляра производного класса. Конструкторы виртуальных базовых классов вызываются первыми, раньше конструкторов невиртуальных базовых классов.

Если виртуальных базовых классов несколько, их конструкторы вызываются в порядке их объявления в списке наследования. За­тем вызываются конструкторы невиртуальных базовых классов в порядке их объявления в списке наследования и, наконец, вызы­вается конструктор производного класса. Если какой-то вирту­альный класс является производным невиртуального базового класса, этот невиртуальный базовый класс конструируется пер­вым (иначе нельзя будет вызвать конструктор виртуального базо­вого класса). Например, в случае объявления

class Basel{};

class Base2{};

class Derived: public Basel, virtual public Base2 { } ;

main()

{

Derived ob;

//…;

return 0;

}

конструкторы вызываются в следующем порядке:

Base2() ;

Basel () ;

Derived();

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

Деструкторы вызываются в порядке, в точности обратном конструкторам.

Соседние файлы в папке ЯзыкС++Глушаков