Добавил:
Upload Опубликованный материал нарушает ваши авторские права? Сообщите нам.
Вуз: Предмет: Файл:
OOP-CPP-AIT-2005.doc
Скачиваний:
9
Добавлен:
16.08.2019
Размер:
477.18 Кб
Скачать
  1. Производные классы

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

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

Иерархия классов позволяет определять новые классы на основе уже имеющихся. Имеющиеся классы обычно называют базовыми (иногда порождающими), а новые классы, формируемые на основе базовых, – производными (порожденными, классами-потомками или наследниками). Производные классы «получают наследство» – данные и методы своих базовых классов, и, кроме того, могут пополняться собственными компонентами (данными и собственными методами). Наследуемые компоненты не перемещаются в производный класс, а остаются в базовых классах. Сообщение, обработку которого не могут выполнить методы производного класса, автоматически передается в базовый класс. Если для обработки сообщения нужны данные, отсутствующие в производном классе, то их пытаются отыскать автоматически и незаметно для программиста в базовом классе.

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

Порождение классов. Для порождения нового класса на основе существующего используется следующая общая форма

class имя_порождаемого_класса : модификатор_доступа

имя_базового_класса

{ объявление_членов;};

При объявлении порождаемого класса модификатор_доступа может принимать значения public, private либо отсутствовать, тогда по умолчанию используется значение private. Раз модификатор доступа может принимать всего два значения: public и private, то и различают два вида наследования – общее (public) и частное (private). В любом случае порожденный класс наследует все члены базового класса, но доступ имеет не ко всем. Ему доступны общие (public) члены базового класса и недоступны частные (private). Для того, чтобы порожденный класс имел доступ к некоторым данным базового класса, в базовом классе их необходимо объявить со спецификацией доступа protected. Члены класса с доступом protected видимы в пределах класса и в любом классе, порожденном из этого класса.

Общее наследование. При общем наследовании порожденный класс имеет доступ к наследуемым членам базового класса с видимостью public и protected. Члены базового класса с видимостью private – недоступны.

спецификация доступа

внутри класса

в порожденном классе

вне класса

private

+

-

-

protected

+

+

-

public

+

+

+

Общее наследование означает, что порожденный класс – это подтип базового класса. Таим образом, порожденный класс представляет собой модификацию базового класса, которая наследует общие и защищенные члены базового класса. Рассмотрим пример общего наследования. Пусть у нас имеется базовый класс student, и мы хотим создать подтип класса sudent – студент-выпускник (grad_student)

class student

{ protected:

char fac[20];

char spec[30];

char name[15];

public:

student(char *f, char *s, char *n)

void print();

};

class grad_student : public student

{ protected:

int year;

char work[30];

public:

grad_student(char *f, char *s, char *n, char *w, int y);

void print();

};

Порожденный класс наследует все данные класса student, имеет доступ к protected и public-членам базового класса. В новом классе добавлено два члена-данных, и порожденный класс переопределяет функцию print().

student :: student(char *f, char *s, char *n)

{ strcpy(fac, f);

strcpy(spec, s);

strcpy(name, n);

}

grad_student :: grad_student(char *f, char *s, char *n,

char *w, int y) : student(f,s,n), year(y) {strcpy(work, w);}

Отметим, что конструктор для базового класса вызывается в списке инициализации.

void student :: print()

{ cout << “fac: ” << fac << “spec: “ << spec

<< ”name: “ << name;

}

void grad_student :: print()

{ student :: print();

cout << “work: “ << work << “year: “ << year;

}

Рассмотрим на примере правила преобразования указателей.

main()

{ student s(“ПС", "УиИТС", "Иванов");

student *ps=&s;

grad_student gs(“ПС”, “УиИТС”, “Петров”, “Полет”, 5);

grad_student *pgs;

ps->print(); // student :: print()

ps=pgs=&gs; // (*)

ps->print(); // student :: print()

pgs->print(); // grad_student :: print()

}

Правило преобразования указателей заключается в том, что указатель на порожденный класс может быть неявно передан в указатель на базовый класс (см. *). При этом переменная-указатель ps на базовый класс может указывать на объекты как базового, так и порожденного класса. Указатель на порожденный класс может указывать только на объекты порожденного класса. Неявные преобразования между порожденным и базовым классами называются предопределенными стандартными преобразованиями:

  • объект порожденного класса неявно преобразуется к объекту базового класса.

  • ссылка на порожденный класс неявно преобразуется к ссылке на базовый класс.

  • указатель на порожденный класс неявно преобразуется к указателю на базовый класс.

Частное наследование. Базовые классы и порожденные классы — это понятия относительные, то есть порожденный класс сам может быть базовым для следующего порождения. При порождении private наследуемые члены базового класса, объявленные как protected и public, становятся членами порожденного класса с видимостью private. При этом члены базового класса с видимостью public и protected становятся недоступными для дальнейших порождений. Цель такого порождения — скрыть классы или элементы классов от использования в дальнейших порождениях. При порождении private не выполняются предопределенные стандартные преобразования:

class X // базовый класс

{...};

class Y : private X // порожденный класс

{...};

X obj1; // объект класса X

Y obj2; // объект класса Y

// obj1=obj2; Ошибка!

Однако, порождение private позволяет отдельным элементам базового класса с видимостью public и protected сохранить свою видимость в порожденном классе. Для этого необходимо

  • в части protected порожденного класса указать те наследуемые члены базового класса с видимостью protected, уточненные именем базового класса, для которых необходимо оставить видимость protected и в порожденном классе;

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

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

class X

{ private:

int n;

protected:

int m;

char s;

public:

void func(int);

};

class Y : private X

{ private:

...............

protected:

...............

X :: s;

public:

...............

X :: func();

};

Говоря о том, что при наследовании можно использовать только два модификатора доступа private и public, мы немножко слукавили, так как возможен и третий вариант – модификатор доступа protected. То, как изменяется доступ к элементам базового класса из производного класса, в зависимости от значения модификаторов наследования, приведено в таблице.

Модификатор наследования →public

protected

private

Модификатор доступа ↓

public

public

protected

private

protected

protected

protected

private

private

нет доступа

нет доступа

нет доступа

Конструкторы и деструкторы при наследовании. Базовый класс, производный класс или оба могут иметь конструкторы и/или деструкторы.

Если и у базового и у производного классов есть конструкторы и деструкторы, то конструкторы выполняются в порядке наследования, а деструкторы – в обратном порядке. То есть если А – базовый класс, В – производный из А, а С – производный из В (А-В-С), то при создании объекта класса С вызов конструкторов будет иметь следующий порядок: конструктор класса А – конструктор класса В – конструктор класса С. Вызов деструкторов при разрушении этого объекта произойдет в обратном порядке: деструктор класса С – деструктор класса В – деструктор класса А.

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

Мы уже говорили о том, что конструкторы могут иметь параметры. При реализации наследования допускается передача параметров для конструкторов производного и базового класса. Если параметрами обладает только конструктор производного класса, то аргументы передаются обычным способом. Однако при необходимости передать аргумент конструктору родительского класса, требуется несколько большее усилие. Прежде всего, нужно позаботится о том, чтобы передать из конструктора производного класса конструктору базового класса. Для этого используется расширенная запись конструктора производного класса.

конструктор_производного_класса (список_формальных_параметров)

: конструктор_базового_класса (список фактических параметров)

{ // тело конструктора производного класса }

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

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

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

  • класс, содержащий виртуальную функцию, базовый в иерархии порождения;

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

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

Указатель на базовый класс может указывать либо на объект базового класса, либо на объект порожденного класса. Выбор функции-члена зависит от того, на объект какого класса при выполнении программы указывает указатель, но не от типа указателя. При отсутствии члена порожденного класса по умолчанию используется виртуальная функция базового класса.

// Выбор виртуальной функции

#include <iostream.h>

class X

{ protected:

int i;

public:

virtual void print()

{cout << “класс X: “ << i;}

};

class Y : public X

{ public:

void print() {cout << “класс Y: “ << i;}

};

main()

{ X x;

X *px=&x; // Указатель на базовый класс

Y y;

x.i=10;

y.i=15;

px->print(); // класс X: 10

px=&y;

px->print(); // класс Y: 15

}

В каждом случае выполняется различная версия функции print(). Выбор динамически зависит от объекта, на который ссылается указатель. В терминологии ООП «объект посылает сообщение print и выбирает свою собственную версию соответствующего метода». Виртуальной может быть только нестатическая функция-член класса. Для порожденного класса функция автоматически становится виртуальной, поэтому ключевое слово virtual можно опустить.

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

Рассмотрим программу, которая вычисляет площади различных фигур. Каждая фигура может порождаться от базового класса figure

class figure

{ protected:

double x, y;

public:

virtual double area() {return(0);} // по умолчанию

};

class rectangle : public figure

{ double height, width;

public:

double area() {return(height*width);}

};

class circle : public figure

{ double r;

public:

double area() {return(PI*r*r);}

};

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

figure *p[N]; // массив указателей на базовый класс

// элементы массива могут ссылаться на

// различные производные классы

for(i=0; i<N; i++)

double tot_area+=p[i]->area();

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

Абстрактные базовые классы. Базовый класс иерархии типа обычно содержит ряд виртуальных функций, которые обеспечивают динамическую типизацию. Часто в самом базовом классе сами виртуальные функции фиктивны и имеют пустое тело. Определенное значение им придается лишь в порожденных классах. Такие функции называются чистыми виртуальными функциями.

Чистая виртуальная функция — это функция-член класса, тело которой не определено.

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

virtual прототип функции = 0;

Например

virtual void func() = 0;

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

Множественное наследование. До сих пор рассматривалось только одиночное наследование, где порожденный класс наследовал элементы одного базового класса. Однако, возможно и множественное наследование. Множественное наследование позволяет порожденному классу наследовать элементы более, чем от одного базового класса. Синтаксис заголовков классов расширяется так, чтобы разрешить создание списка базовых классов и обозначения их уровня доступа:

class X

{...};

class Y

{...};

class Z

{...};

class A : public X, public Y, public Z

{...};

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

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

  • если в порожденном классе используется член с таким же именем, как в одном из базовых классов;

  • когда в нескольких базовых классах определены члены с одинаковыми именами.

В этих случаях необходимо использовать оператор разрешения контекста для уточнения элемента, к которому осуществляется доступ, именем базового класса.

class X

{int key;};

class Y

{int key;};

class Z

{int key;};

class A : public X, public Y, public Z

{int key;

...............

key=X :: key + Y :: key + Z :: key;

...............

};

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

  1. конструкторы базовых классов в порядке их задания;

  2. конструкторы членов, являющихся объектами класса;

  3. конструктор порожденного класса.

Рассмотрим пример.

class X

{ X (char*) {...};

~X();

};

class Y

{Y (char*) {...};

~Y();

};

class Z

{Z (char*) {...};

~Z();

};

class A : public X, public Y, public Z

{...............

A(int) : X(“aaa”), Y(“bbb”), Z(“ccc”)

{тело конструктора класса A}

};

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

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

Такая иерархия порождения несет двусмысленность при доступе к наследуемым членам класса X и может привести к ошибкам. В этом случае класс X будет дважды присутствовать в Z, что не экономит память. Чтобы избежать двусмысленности пользователь должен знать детали порождения, но это противоречит объектно-ориентированному подходу, одна из целей которого — избавить пользователя от знания деталей порождения. Существует два выхода для разрешения такой ситуации:

  • преобразование порождения из нескольких классов в порождение из одного класса и объявление дружественных классов.

class Y : public X

{friend class Z; };// класс Z имеет доступ к Y

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

Такой механизм заключается в возможности создания виртуальных базовых классов. Базовый класс определяется как виртуальный заданием ключевого слова virtual в списке порождения перед именем базового класса или указанием типа наследования

сlass имя_порожденного_класса :

virtual public_или_private имя_базового_класса

{// тело_класса }:

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

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

  • конструкторы виртуальных базовых классов выполняются до конструкторов не виртуальных базовых классов, независимо от того, как эти классы заданы в списке порождения;

  • если класс имеет несколько виртуальных базовых классов, то конструкторы этих классов вызываются в порядке объявления виртуальных базовых классов в списке порождения;

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

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

Соседние файлы в предмете [НЕСОРТИРОВАННОЕ]