Глава 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();
Если иерархия классов содержит несколько экземпляров виртуального базового класса, этот базовый класс конструируется только один раз. Если все же существуют как виртуальный, так и невиртуальный экземпляры этого базового класса, конструктор базового класса вызывается один раз для всех виртуальных экземпляров этого базового класса, а затем еще раз для каждого невиртуального экземпляра этого базового класса.
Деструкторы вызываются в порядке, в точности обратном конструкторам.