Class base

. . .

Base(пар-ры конструктора Base)

. . .

F(int),

Class Base

Объявления доступа.

· Объявления доступа дают возможность сделать опять защищенными или открытыми защищенные или открытые члены закрытого базового класса в производном классе соответственно.

· Объявления доступа позволяют сделать снова открытыми открытые члены базового класса в защищенном производном. (Ни этот, ни предыдущий пункт не выполняется, если в производном классе объявлены члены с теми же именами, что и в базовом классе).

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

{

int a;

void g();

public:

int b,c;

void f(),

g(int);

};

class Derived : private Base

{

public:

Base::a; // Ошибка нельзя сделать a - public

Base::b; // Вновь делает b public

int c;

Base::c; // Ошибка: нельзя с объявлять дважды

Base::f; // Вновь делает все f() public

Base::g; // Ошибка: функции g() имеют различное

// ограничение доступа

};

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

class Base { . . . };

class Derived : public Base { . . .};

Derived::Derived(пар-ры) – конструктор производного класса

{ . . . }; - реализация тела конструктора

В рассмотренном случае в базовом классе Base должен быть описан конструктор Base() или не должно быть ни одного конструктора, т.е. использоваться конструктор по умолчанию. Если в базовом классе описан хотя бы один конструктор, отличный от конструктора по умолчанию, т.е. без параметров, то при описании конструктора производного класса необходимо вызывать нужный конструктор класса Base:

Derived::Derived(пар-ры конструктора Derived) :

{ . . . }; - реализация тела конструктора

Параметры конструктора класса Base должны быть подмножеством параметра соответствующего конструктора класса Derived, либо задаваться константами. Допустим смешанный вариант. В любом случае последовательность действий при конструировании объекта производного класса выглядит так:

- вызов конструктора класса Derived

- вызов конструктора базового для класса Derived класса Base

- вызов конструктора базового для класса Base (если есть)

- выполнение тела реализации конструктора базового для класса Base

- выполнение тела реализации конструктора класса Base

- выполнение тела реализации конструктора класса Derived

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

~имя_класса();

У деструктора не может быть параметров (даже типа void), и деструктор не имеет возможности возвращать какой-либо результат, даже типа void. Статус доступа деструктора по умолчанию public (т.е. деструктор доступен во всей области действия определения класса). В несложных классах деструктор обычно определяется по умолчанию.

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

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

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

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

Множественное наследование и виртуальные базовые классы.Класс называют непосредственным (прямым) базовым классом(прямой базой), если он входит в список базовых при определении класса. В то же время для производного класса могут существовать косвенные или непрямые предшественники, которые служат базовыми для классов, входящих в список базовых. Если некоторый класс Аявляется базовым для В,а Весть базовый класс дляС,то класс Вявляется непосредственным базовым классом для С, а класс Аявляется непрямым базовым классом для С. Обращение к компоненту ха, входящему в А и унаследованному последовательно классами Ви С, можно обозначить в классе С,либо как А::ха, либо как В::ха. Обе конструкции обеспечивают обращение к элементу хакласса А.

Производные классы принято изображать ниже базовых. Именно в таком порядке их тексты размещаются в листинге программы и рассматриваются компилятором. Класс может иметь несколько непосредственных базовых классов, т.е. может быть порожден из любого числа базовых классов, например,

class X1 { ... };

class X2 { ... };

class X3 { ... };

class Y1: public X1, public X2, public X3 { ... };

Наличие нескольких прямых базовых классов называют множественным наследованием.

Определения базовых классов должны предшествовать их использованию в качестве базовых. При множественном наследовании никакой класс не может больше одного раза использоваться в качестве непосредственного базового. Однако класс может больше одного раза быть непрямым базовым классом:

class X { ...; f () ; ... };

class Y: public X { ... };

class Z: public X { ... };

class D: public Y, public Z { ... };

В данном примере класс Х дважды опосредовано наследуется классом D.

Проиллюстрированное дублирование класса соответствует включению в производный объект нескольких объектов базового класса. В нашем примере существуют два объекта класса Х,и поэтому для устранения возможных неоднозначностей вне объектов класса Dнужно обращаться к конкретному компоненту класса Х, используя полную квалификацию: D::Y::X::f()илиD::Z::X::f(). Внутри объекта класса Dобращения упрощаются Y::X::f()или Z::X::f(), но тоже содержат квалификацию.

Чтобы устранить дублирование объектов непрямого базового класса при множественном наследовании, этот базовый класс объявляют виртуальным. Для этого в списке базовых классов перед именем класса необходимо поместить ключевое слово virtual. Например, класс Хбудет виртуальным базовым классом при таком описании:

class X { ... f(); ... };

class Y: virtual public X { ... };

class Z: virtual public X { ... };

class D: public Y, public Z { ... };

Теперь класс D будет включать только один экземпляр Х, доступ к которому равноправно имеют классы Yи Z.

Обратите внимание, что размеры производных классов при отсутствии виртуальных базовых равны сумме длин их компонентов и длин унаследованных базовых классов. «Накладные расходы» памяти здесь отсутствуют.

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

class X { ... };

class Y: virtual public X { ... };

class Z: virtual public X { ... };

class B: virtual public X { ... };

class C: virtual public X { ... };

class E: public X { ... };

class D: public X { ... };

class A: public D,public B,public Y,public Z,public C,public E { ... };

В данном примере объект класса А включает три экземпляра объектов класса Х: один виртуальный, совместно используемый классами B, Y, C, Z,и два не виртуальных относящихся соответственно к классам Dи E.Таким образом, можно констатировать, что виртуальность класса в иерархии производных классов является не свойством класса как такового, а результатом особенностей процедуры наследования.

Возможны и другие комбинации виртуальных и не виртуальных базовых классов. Например:

class BB { ... };

class AA: virtual public BB { ... };

class CC: virtual public BB { ... };

class DD: public AA, public CC, public virtual BB { ... };

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

class X { public: int d; ... };

class Y { public: int d; ... };

class Z: public X, public Y

{

public:

int d;

...

d=X::d + Y::d;

...

};

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

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

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

Рассмотрим теперь, как ведут себя при наследовании не виртуальные компонентные функции с одинаковыми именами, типами и сигнатурами параметров.

Если в базовом классе определена некоторая компонентная функция, то такая же функция (с тем же именем, того же типа и с тем же набором и типами параметров) может быть введена в производном классе. Рассмотрим следующее определение классов:

{

public:

void funс (int i)

{ printf(" base::i =",i); };

};

class derived: public base

{

public:

void funс (int i)

{ printf(" der::i =",i); };

};

В данном случае внешне одинаковые функции void func (int)определены в базовом классе baseи в производном классе derived.

В теле класса derivedобращение к функции func(), принадлежащей классу base, может быть выполнено с помощью полного квалифицированного имени, явно включающего имя базового класса: base::func(). При обращении в классе derivedк такой же (по внешнему виду) функции, принадлежащей классу derived, достаточно использовать имя func() без предшествующего квалификатора.

В программе, где определены и доступы оба класса baseи derived, обращения к функциям func()могут быть выполнены с помощью указателей на объекты соответствующих классов:

void main(void) {

base B, *bp = &B;

derived D, *dp = &D;

base *pbd = &D;

bp->func(1); // Печатает : base::i = 1

dp->func(5); // Печатает : der::i = 5

pbd->func(4); // Печатает : base::i = 4

};

В программе введены три указателя на объекты разных классов. Следует обратить внимание на инициализацию указателя pbd. В ней адрес объекта производного класса (объекта D) присваивается указателю на объект его прямого базового класса (base *). При этом выполняется стандартное преобразование указателей, предусмотренное синтаксисом языка Си++. Обратное образование, т.е. преобразование указателя на объект базового класса в указатель на объект производного класса, невозможно (запрещено синтаксисом). Обращения к функциям классов baseи derived с помощью указателей bpи dp не представляют особого интереса. Вызовpbd->func()требуется прокомментировать. Указатель pbd имеет тип base*, однако его значение - адрес объекта D класса derived.

Какая же из функций base::func()или derived::func() вызывает при обращении pbd->func()? Результат выполнения программы показывает, что вызывается функция из базового класса. Именно такой вызов предусмотрен синтаксисом языка Си++, т.е. выбор функции (не виртуальной) зависит только от типа указателя, но не от его значения. «Настроив» указатель базового класса на объект производного класса, не удается с помощью этого указателя вызвать функцию из производного класса.

Пусть в этом классе определена компонентная функция void show(). Доступ к функции show() производного класса возможен только с помощью явного указания области видимости:

имя_производного_класса::show();

либо с использованием имени конкретного объекта:

имя_объекта_производного_класса.show();

В обоих случаях выбор нужной функции выполняется при написании исходного текста программы и не изменяется после компиляции. Такой режим называется ранним или статическим связыванием.

Большую гибкость (особенно при использовании уже готовых библиотек классов) обеспечивает позднее (отложенное), или динамическое связывание, которое предоставляется механизмом виртуальных функций. Любая нестатическая функция базового класса может быть сделана виртуальной, если в ее объявлении использовать спецификатор virtual. Прежде чем объяснить преимущества динамического связывания, приведем пример. Опишем в базовом классе виртуальную функцию и введем два производных класса, где определим функции с такими же прототипами, но без спецификатора virtual.