Добавил:
Upload Опубликованный материал нарушает ваши авторские права? Сообщите нам.
Вуз: Предмет: Файл:
Книга C++.doc
Скачиваний:
24
Добавлен:
10.11.2019
Размер:
2.48 Mб
Скачать

Абстрактные классы

Очень часто в базовом классе определяется виртуальная функция, которая не выполняет каких-либо значимых действий. Вы уже встречались с понятием "иерархия объектов". Напомним, что чем ниже находится класс в лестнице иерархии, тем он более конкретезирован. Другими словами, базовый класс обычно не определяет законченный тип. Очень часто получается, что базовый класс не имеет какой-либо практической ценности для его непосредственного применения в программе. Однако он имеет ценность для формирования производных классов.

Давайте, снова обратимся к примеру из раздела "Виртуальные функции". Мы использовали в программе создание объекта класса vehicle (транспортное средство) только для придания примеру большей наглядности. В реальной же программе, нам вряд ли пригодится объект класса vehicle. Аналогично и в реальном мире, мы редко используем объекты класса транспортное средство. Нас больше интересует конкретная реализация возможностей этого класса, которую нам дают производные из него классы car (легковая машина), truck (грузовик), boat (лодка). Рассматривая работу функции message() класса vehicle, мы с Вами обращали внимание, что при выполнении нашей программы только один раз (если не принимать в расчет первый вызов процедуры message(), когда был определен объект класса vehicle) из трех вызывалась функция message() класса vehicle. Если же в класс truck добавить свою функцию message(), то реализация функции message() в классе vehicle не будет представлять никакой ценности. Таким образом, мы можем сделать два очень важных на данном этапе вывода:

  1. класс vehicle нам ценен только как базовый класс

  2. функция message() член класса vehicle стала нужна только для того, чтобы ее переопределили в производных классах.

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

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

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

Теперь, когда Вы знаете что такое чисто виртуальная функция, мы можем рассмотреть определение абстрактного класса: класс имеющий хотя бы одну чисто виртуальную функцию, называется абстрактным. Абстрактные классы не бывают изолированными, то есть всегда абстрактный класс должен быть наследуемым. Почему? Так как у чистой виртуальной функции нет тела, то создать экземпляр абстрактного класса невозможно. Более того, если у Вас есть класс, который наследуется от абстрактного класса и этот класс НЕ переопределяет чисто виртуальную функцию, то он тоже будет абстрактным. Это замечательное свойство дает нам возмжность ПОШАГОВО конкретезировать класс.

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

Для продолжения дальнейшего исследования страны множественного наследования нам необходимо познакомиться с одним понятием. В компьютерной литературе, которая сейчас представлена в достаточном колличестве на рынке, при рассмотрении вопроса связаного с множественным наследованием Вы можете натолкнуться на подобные высказывания: "Для описания иерархии множественного наследования часто используют прямой ациклический граф" или "Такие родительские отношения описываются ориентированным ациклическим графом наследования". Согласитесь, звучит довольно таки страннавато (или даже устрашающе), если Вы не посвящали пять лет своей жизни изучению точных наук. В подходящей обстановке можно даже спросить своего любимого человека: "Дорогая (дорогой), а что ты думаешь об ориентированных ациклических графах?" (примечание: авторы этих строк не несут ответсвенности за любые последствия, связанные с такими вопросами). На вопрос: "А что это такое?", Вы можете ответить следующее: ориентированный ациклический граф наследования - это граф, узлы которого являются классами, а ориентированные ребра направлены от производных классов к базовым.

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

Прямой ациклический граф множественного насследования

На рисунке представлены четыре класса. Стрелочка указывает на базовый класс. Таким образом, у нас Parent является базовым классом для классов: Child1 и Child2. Класс GrandChild наследуется от классов Child1 , Child2.

Как видно из приведенного примера, если оба базовых класса используются при создании нового производного класса, то такой класс будет иметь два подобъекта общего предка. Обычно, такое дублирование нежелательно. О повторном наследование (второй проблеме множественного насследования) Мейер, автор языка программирования Eiffel, пишет следующее: "Одно тонкое затруднение при использовании множественного наследования встречается, когда класс является наследником другого по нескольким линиям. Если в языке разрешено множественное наследование, рано или поздно кто-нибудь напишет класс D, который наследуется от B и C, которые, в свою очередь, наследуются от A. Эта ситуация называется повторным насследованием, и с ней нужно корректно обращаться".

Уже известный нам классик объектно-ориентированого анализа Гради Буч предлагает следующие варианты решения даной проблемы: "Проблема повторного наследования решается тремя способами. Во-первых, можно его запретить, отслеживая при компиляции. Так сделано в языках Smalltalk и Eiffel. Во-вторых, можно явно развести две копии унаследованого элемента, добавляя к именам префиксы в виде имени класса-источника (это один из подходов, принятых в С++). В третьих, можно рассматривать множественное ссылки на один и тот же класс, как обозначающие один и тот же класс. Так поступают в С++, где повторяющийся базовый класс определяется как виртуальный базовый класс".

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

Допустим, класс Parent на самом деле называется Vehicle (транспортное средство). В его протоколе содержится private член-данных int topSpeed (предельная скорость).

Предположим также, что класс Child1 на самом деле называется Boat (корабль), а класс Child2 - называется Plane (самолет). Наконец, класс GrandChild на самом деле называется SeaPlane (морской самолет).

Для SeaPlane желательно наследовать Boat::topSpeed и Plane::topSpeed. Объект класса SeaPlane имеет различную предельную скорость в зависимости от того, действует он как корабль или как самолет.

С другой стороны, допустим, класс Parent назван DomesticAnimal (домашнее животное), класс Child1 назван Cow (коров), класс Child2 назван Buffalo (бык), а класс GrandChild назван Beefalo (теленок). Предположим также, что интерфейс класса DomesticAnimal включает члены-данные int weight (вес), float price (цена), char color[20] (цвет). В такой ситуации, для нас совсем нежелательно, чтобы протокол класса Beefalo имел по два экземпляра переменных weight, price и color. Необходимо предотвратить такое дублирование. Как Вы уже знаете, сделать это позволяет использование виртуальных базовых классов. Рассмотрим практическую реализацию всего сказаного выше.

#include <iostream.h>

#include <string.h>

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

protected:

int weight;

float price;

char color[20];

public:

DomesticAnimal(){

//конструктор по умолчанию

cout<<"Default Constructor for class DomesticAnimal\n";

weight=0;

price=0;

strcpy(color,"None");

}

DomesticAnimal(int aWeight, float aPrice, char* aColor){

//конструктор принимающий параметры

cout<<"Multiple params Constructor for class DomesticAnimal\n";

weight=aWeight;

price=aPrice;

strcpy(color,aColor);

}

virtual void Print(){

//выведем на экран значения членов-данных

cout<<"The weight = "<<weight<<endl;

cout<<"The price = "<<price<<" (UAH)"<<endl;

cout<<"The color = "<<color<<endl;

}

};

class Cow:public virtual DomesticAnimal{

//класс Cow наследуется от виртуального базового

//класса DomesticAnimals

public:

Cow(){

//конструктор по умолчанию

cout<<"Default Constructor for class Cow\n";

}

Cow(int aWeight, float aPrice, char* aColor){

//конструктор принимающий параметры

cout<<"Multiple params Constructor for class Cow\n";

weight=aWeight;

price=aPrice;

strcpy(color, aColor);

}

void Print(){

//распечатаем свойства объекта Cow

cout<<"\n\nThe COW has properties:\n";

//вызов базового метода

DomesticAnimal::Print();

}

};

class Buffalo: public virtual DomesticAnimal

{

public:

Buffalo(){

//конструктор по умолчанию

cout<<"Default Constructor for class Buffalo\n";

}

Buffalo(int aWeight,float aPrice, char* aColor)

{//конструктор принимающий параметры

cout<<"Multiple params Constructor for class Buffalo\n";

weight=aWeight;

price=aPrice;

strcpy(color,aColor);

}

void Print(){

//распечатаем свойства объекта Cow

cout<<"\n\nThe BUFFALO has the properties:\n";

//вызов базового метода

DomesticAnimal::Print();

}

};

class Beefalo:public Cow, public Buffalo{

//случай множественного наследования

public:

Beefalo(int aWeight, float aPrice, char* aColor){

//конструктор принимающий параметры

cout<<"Multiple params Constructor for class Beefalo\n";

weight=aWeight;

price=aPrice;

strcpy(color,aColor);

}

void Print(){

//распечатаем свойства объекта Beefalo

cout<<"\n\nThe BEEFALO has the properties:\n";

//вызов базового метода

DomesticAnimal::Print();

}

};

void main()

{

cout<<"\t\t\t...Testing...\n";

//начинаем тестирование

cout<<"\tCreating Cow object...\n\n";

//создадим экземпляр класса Cow

Cow aCow(1400,2808.25,"Black and white");

cout<<"\n\tCreating Beefalo object...\n\n";

//создадим экземпляр класса Beefalo

Beefalo aBeefalo(1700,1600,"Brown and black");

aCow.Print();

aBeefalo.Print();

}

Результат выполнения программы:

...Testing...

Creating Cow object...

Default Constructor for class DomesticAnimal

Multiple params Constructor for class Cow

Creating Beefalo object...

Default Constructor for class DomesticAnimal

Default Constructor for class Cow

Default Constructor for class Buffalo

Multiple params Constructor for class Beefalo

The COW has properties:

The weight = 1400

The price = 2808.25 (UAH)

The color = Black and white

The BEEFALO has the properties:

The weight = 1700

The price = 1600 (UAH)

The color = Brown and black

Код, приведенный выше, решает проблему класса Beefalo, который многократно наследует члены-данных weight, price, color. Объекты класса Beefalo имеют по одному члену-данных для веса, цены и цвета. Использование ключевого слова virtual в классе Cow (pulic virtual DomesticAnimal) и классе Buffalo (pulic virtual DomesticAnimal) предотвращает многократное копирование членов-данных weight, price, color из предков класса Beefalo

В завершении, рассмотрим следующий вопрос: "А что произойдет, если из объявлений классов Cow и Buffalo убрать ключевое слово virtual?". Компилятор выдаст сообщение об ошибке. В Beefalo унаследовано несколько копий членов-данных weight, price и color, следовательно, конструктор с несколькими параметрами в классе Beefalo становится недействительным.

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

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

Теперь, когда Вы надежно подкреплены теоретическими знаниями, подозреваем, что Вам не терпиться рассмотреть Практический пример.