68: protected:
69: COLOR itsColor;
70: };
71:
72: class Dog : public Mammal
73: {
74: public:
75: Dog(int age, COLOR color ):
76: Mammal(age), itsColor(color)
77: { cout << "Dog constructor...n"; }
78: virtual ~Dog() { cout << "Dog destructor...n"; }
79: virtual void Speak()const { cout << "Woof!... n"; }
80: virtual void 51eep() const { cout << "Dog snoring...n"; }
81: virtual void Eat() const { cout << "0og eating...n"; }
82: virtual void Move() const { cout << "Dog running...n"; }
83: virtual void Reproduce() const
84: { cout << "Dogs reproducing...n"; }
85:
86: protected:
87: COLOR itsColor;
88: };
89:
90: int main()
91: {
92: Animal *pAnimal=0;
93: int choice;
94: bool fQuit = false;
95:
96: while (1)
97: {
98: cout << "(1)Dog (2)Horse (3)Fish(0)Quit: ";
99: cin >> choice; 100:
101: switch (choice)
102: {
103: case 1: pAnimal = new Dog(5,Brown);
104: break;
105: case 2: pAnimal = new Horse(4,Black);
106: break;
107: case 3: pAnimal = new
108: break;
109: default: fQuit = true
110: break;
111: }
112: if (fQuit)
113: break;
114:
115: pAnimal->Speak();
116: pAnimal->Eat();
117: pAnimal->Reproduce();
118: pAnimal->Move();
119: pAnimal->Sleep();
120: delete pAnimal;
121: cout << "n";
122: }
123: return 0;
124: }
Результат:
(1)Dog (2)Horse (3)Bird (0)Quit: 1
Animal constructor. . .
Mammal constructor...
Dog constructor...
Woof!...
Dog eating. . .
Dog reproducing....
Dog running...
Dog snoring...
Dog destructor...
Mammal destructor...
Animal destructor...
(1)Dog (2)Horse (3)Bird (0)Quit: 0
Анализ: В строках 7—21 объявляется абстрактный тип данных Animal. Единственный метод этого класса, не являющийся чистой виртуальной функцией, это общий для объектов всех производных классов метод itsAge. Остальные пять методов — Sleep(), Eat(), Reproduce(), Move() и Speak() — объявлены как чистые виртуальные функции.
Класс Mammal производится от Animal в строках 29—37 и не содержит никаких данных. В нем замещается функция Reproduce(), чтобы задать способ размножения, общий для всех млекопитающих. Класс Fish производится непосредственно от класса Animal, поэтому функция Reproduce() в нем замещается иначе, чем в классе Mammal (и это соответствует реальности).
Во всех других классах, производимых от класса Mammal, теперь нет необходимости замещать общий для всех метод Reproduce(), хотя при желании это можно сделать для определенного класса, как, например, в нашей программе это было сделано в строке 83 для класса Dog. Все остальные чистые виртуальные функции были замещены в классах Fish, Horse и Dog, поэтому для каждого из них можно создавать соответствующие объекты.
В теле программы используется указатель класса Animal, с помощью которого делаются ссылки на все объекты производных классов. В зависимости от того, с каким объектом связан указатель в текущий момент, вызываются соответствующие виртуальные функции.
При попытке создать объекты для классов абстрактных типов данных Animal или Mammal компилятор покажет сообщение об ошибке.
Когда следует использовать абстрактные типы данных
В одних примерах программ, рассмотренных нами ранее, класс Animal являлся абстрактным типом данных, в других — нет. В каких же случаях нужно объявлять класс как абстрактный тип данных?
Нет никаких правил, которые требовали бы объявления класса как абстрактного. Программист принимает решение о создании абстрактного типа данных, основываясь на том, какую роль играет этот класс в программе. Так, если вы хотите смоделировать виртуальную ферму или зоопарк, то имеет смысл класс Animal объявить как абстрактный и для создания объектов производить от него другие классы, такие как Dog.
Если же вы хотите смоделировать виртуальную псарню, то теперь класс Dog будет абстрактным, от которого можно производить подклассы, представляющие разные породы собак. Количество уровней абстрактных классов следует выбирать в зависимости от того, насколько детально вы хотите смоделировать реальный объект или явление.
Рекомендуется:Используйте абстрактные типы данных для создания общего интерфейса для всех производных классов. Обязательно замещайте в производных классах все чистые виртуальные функции. Объявляйте все функции, которые нужно замещать в производных классах, как чистые виртуальные функции.
Не рекомендуется:Не пытайтесь создать объектабстрактного класса.
Логика использования абстрактных классов
В последнее время в программировании на C++ активно используется концепция создания абстрактных логических конструкций. С помощью таких конструкций можно находить решения для многих общих задач и создавать при этом программы, которые легко читаются и документируются. Рассмотрим пример создания логической конструкции с использованием наследования классов.
Представим, что нужно создать класс Timer, который умеет отсчитывать секунды. Такой класс может иметь целочисленную переменную-член itsSeconds, а также метод, осуществляющий приращение переменной itsSeconds.
Теперь предположим, что программа должна отслеживать и сообщать о каждом изменении переменной itsSeconds. Первое решение, которое приходит на ум, — это добавить в класс Timer метод уведомления об изменении переменной-члена. Но логически это не совсем верно, так как программа уведомления может быть достаточно сложной и по сути своей не является логической частью программы отсчета времени.
Гораздо логичнее рассматривать программу отслеживания и информирования об изменении переменной как абстрактный класс, который в равной степени может использоваться как с программой отсчета времени, так и с любой другой программой с периодически изменяющимися переменными.
Таким образом, лучшим решением будет создание абстрактного класса обозревателя Observer с чистой виртуальной функцией Update().
Теперь создадим второй абстрактный класс — Subject. Он содержит массив объектов класса Observer. Кроме того, в нем объявлены два дополнительных метода: Register(), который регистрирует объекты класса Observer, и Notify(), который отслеживает изменения указанной переменной.
Эта конструкция классов может затем использоваться во многих программах. Те классы, которые будут отслеживать изменения и сообщать о них, наследуются от класса Observer. Класс Timer в нашем примере наследуется от класса Subject. При изменении контролируемой переменной (в нашем примере — itsSeconds) вызывается метод Notify(), унаследованный от класса Subject.
Наконец, можно создать новый класс ObserverTimer, унаследованный сразу от двух базовых классов — Observer и Timer, который будет сочетать в себе возможности отсчитывать время и сообщать об этом.
Пара слов о множественном наследовании, абстрактных типах данных и языке Java
Многие программисты знают, что в основу языка Java положен C++. Также известно, что создатели языка Java удалили из него возможность множественного наследования потому, что, по их мнению, это средство слишком усложняет программный код и идет в разрез с концепцией упрощения программных кодов, положенной в основу Java. С точки зрения создателей Java, 90% всех возможностей, предоставляемых множественным наследованием, можно получить с помощью интерфейса.
Интерфейс в терминологии Java представляет собой нечто подобное абстрактному типу данных, в том смысле, что в нем также определяются функции, которые могут быть реализованы только в производных классах. Но новые классы не производятся непосредственно от интерфейса. Классы производят от других классов и в них передаются функции интерфейса, что напоминает множественное наследование. Так, союз абстрактных классов и множественного наследования породил на свет аналог классов- мандатов, в результате чего удалось избежать чрезмерного усложнения программных кодов, как в случае с множественным наследованием. Кроме того, поскольку интерфейсы не содержат ни выполняемых функций, ни переменных-членов, отпадает необходимость в виртуальном наследовании.
Насколько удобны или целесообразны эти изменения, зависит от привычек конкретного программиста. Во всяком случае, если вы хорошо разберетесь в множественном наследовании и абстрактных типах данных языка C++, то это послужит хорошей базой при изучении и освоении последних достижений и тенденций программирования, реализованных в языке Java (если у вас возникнет интерес к нему).
Использование логических конструкций в языках C++ и Java подробно рассматривается в следующей статье: Robert Martin, C++ and Java: А Critical Comparison // C++ Report. — January, 1997.
Резюме
Сегодня вы познакомились с методами преодоления некоторых ограничений одиночного наследования. Вы узнали об опасности передачи вверх по иерархии классов интерфейса производных функций и об ограничениях приведения типа данных объектов базового класса к производным классам во время выполнения программы. Кроме того, вы узнали, когда и как используется множественное наследование классов, какие проблемы при этом могут возникнуть и как их преодолеть.