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

Глава 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() « ' ';}

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

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