Проблемы с множественным наследованием

Хотя множественное наследование дает ряд преимуществ по сравнение с одиночным, многие программисты с неохотой используют его. Основная проблема состоит в том, что многие компиляторы C++ все еще не поддерживают множественное наследование; это осложняет отладку программы, тем более что все возможности, реализуемые этим методом, можно получить и без него.

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

Указание виртуального наследования при объявлении класса

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

Пример 1:

classHorse : virtual public Animal class Bird : virtual public Animal '. class Pegasus: public Horse,public Bird

Пример 2:

class Schnauzer : virtual public 0og class Poodle ; virtual public 0og class Schnoodle : public Schnauzer, publiс Poodle

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

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

Классы-мандаты

Промежуточным решением между одиночным и множественным наследованием классов может быть использование классов-мандатов. Так, класс Horse можно произвести от двух базовых классов — Animal и Displayable, причем последний добавляет только некоторые методы отображения объектов на экране.

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

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

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

Классы-мандаты (capability class) иногда еще называют миксинами (mixin). Этот термин произошел от названия десерта, представляющего собой смесь пирожного с мороженым, политую сверху шоколадной глазурью. Этот десерт продавался в супермаркетах Sommerville в штате Массачусетс. Видимо, это блюдо когда-то попробовал один из программистов, занимающийся разработкой средств объектно-ориентированного программирования для языка SCOOPS, где этот термин впервые появился.

Абстрактные типы данных

В объектном программировании довольно часто создаются иерархии логически связанных классов. Например, представим класс Shape, от которого произведены классы Rectangle и Circle. Затем от класса Rectangle производится класс Sguare, как частный вид прямоугольника.

В каждом из производных классов замещаются методы Draw(), GetArea() и др. Основной костяк программы с классом Shape и производными от него Rectangle и Circle показан в листинге 13.7.

Листинг 13.7. Классы семейства Shape

1: // Листинг 13.7. Классы семейства Shape

2:

3: #include <iostream.h>

4:

5:

6: class Shape

7: {

8: public:

9: Shape(){ }

10: virtual ~Shape() { }

11: virtual long GetArea() { return -1; }

12: virtual long GetPerim() { return -1; }

13: virtual void Draw() { }

14: private:

15: };

16:

17: class Circle : public Shape

18: {

19: public:

20: Circle(int radius):itsRadius(radius) { }

21: ~Circle() { }

22: long GetArea() { return 3 * itsRadius * itsRadius; }

23: long GetPerim() { return 6 * itsRadius; }

24: void Draw();

25: private:

26: int itsRadius;

27: int itsCircumference;

28: };

29:

30: void Circle::Draw()

31: {

32: cout << "Circle drawing routine here!\n";

33: }

34:

35:

36: class Rectangle : public Shape

37: {

38: public:

39: Rectangle(int len, int width);

40: itsLength(len), itsWidth(width) { }

41: virtual ~Rectangle() { }

42: virtual long GetArea() { return itsLength * itsWidth; }

43: virtual long GetPerim() { return 2*itsLength + 2*itsWidth; }

44: virtual int GetLength() { return itsLength; }

45: virtual int GetWidth() { return itsWidth; }

46: virtual void Draw();

47: private:

48: int itsWidth;

49: int itsLength;

50: };

51:

52: void Rectangle::Draw()

53: {

54: for (int i = 0; i<itsLength; i++)

55: {

56: for (int j = 0; j<itsWidth; j++)

57: cout << "x ";

58:

59: cout << "\n";

60: }

61: }

62:

63: class Square : public Rectangle

64: {

65: public:

66: Square(int len);

67: Square(int len, int width);

68: ~Square() { }

69: long GetPerim() { return 4 * GetLength();}

70: };

71:

72: Square::Square(int len):

73: Rectangle(len,len)

74: { }

75:

76: Square::Square(int len, int width):

77: Rectangle(len,width) 78:

79: {

80: if (GetLength() != GetWidth())

81: cout << "Error, not a sguare... a Rectangle??\n";

82: }

83:

84: int main()

85: {

86: int choice;

87: bool fQuit = false;

88: Shape * sp;

89:

90: while ( ! fQuit )

91: {

92: cout << "(1)Circle (2)Rectangle (3)Square (0)Quit:";

93: cin >> choice;

94:

95: switch (choice)

96: {

97: case 0: fQuit = true;

98: break;

99: case 1: sp = new Circle(5);

100: break;

101: case 2: sp = new Rectangle(4,6);

102: break;

103: case 3: sp = new Square(5);

104: break;

105: default: cout << "Please enter a number between 0 and 3" << endl;

106: continue;

107: break;

108: }

109: if(! fQuit)

110: sp->Draw();

111: delete sp;

112: cout << "\n";

113: }

114: return 0;

115: }

Результат:

(1)Circle (2)Rectangle (3)Square (0)Quit: 2

x x x x x x

X X X X X X

X X X X X X

X X X X X X

(1)Circle (2)Rectangle (3)Square (0)Quit:3

X X X X X

X X X X x

X X X X X

X X X X X

X X X X X

(1)Circle (2)Rectangle (3)Square (0)Quit:0

Анализ: В строках 6—15 объявляется класс Shape. Методы GetArea() и GetPerim() возвращают -1 как сообщение об ошибке, а метод Draw() не выполняет никаких действий. Давайте подумаем, можно ли в принципе нарисовать форму? Можно нарисовать окружность, прямоугольник или квадрат, но форма — это абстракция, которую невозможно изобразить.

Класс Circle производится от класса Shape, и в нем замещаются три виртуальных метода. Обратите внимание, что в данном случае нет необходимости использовать ключевое слово virtual, поскольку виртуальность функций наследуется в производном классе. Тем не менее для напоминания о виртуальности используемых функций не лишним будет явно указать это.

Класс Square производится от класса Rectangle и наследует от него все методы, причем метод GetPerim() замещается в новом классе.

Все методы должны функционировать нормально в производных классах, но не в базовом классе Shape, поскольку невозможно создать экземпляр формы как таковой. Программа должна быть защищена от попытки пользователя создать объект этого класса. Класс Shape существует только для того, чтобы поддерживать интерфейс, общий для всех производных классов, поэтому об этом типе данных говорят как об абстрактном, или ADT (Abstract Data Туре).

Абстрактный класс данных представляет общую концепцию, такую как форма, а не отдельные объекты, такие как окружность или квадрат. В C++ ADT по отношению к другим классам всегда выступает как базовый, для которого невозможно создать функциональный объект абстрактного класса.

Чистые виртуальные функции

C++ поддерживает создание абстрактных типов данных с чистыми виртуальными функциями. Чистыми виртуальными функциями называются такие, которые инициализируются нулевым значением, например:

virtual void Draw() = 0;

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

• невозможность создания объекта этого класса;

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

Любой класс, произведенный от ADT, унаследует от него чистую виртуальную функцию, которую необходимо будет заместить, чтобы получить возможность создавать объекты этого класса. Так, если класс Rectangle наследуется от класса Shape, который содержит три чистые виртуальные функции, то в классе Rectangle должны быть замещены все эти три функции, иначе он тоже будет ADT. В листинге 13.8 изменено объявление классa Shape таким образом, чтобы он стал абстрактным типом данных. Остальная часть листинга 13.7 не изменилась, поэтому не приводится. Просто замените объявление класса в строках 7—16 листинга 13.7 листингом 13.8 и запустите программу.

Листинг 13.8. Абстрактные типы данных

1: класс Shape

2: {

3: public:

4: Shape(){ }

5: ~Shape(){ }.

6: virtual long GetArea() = 0; // ошибка

7: virtual long GetPerim()= 0;

8: virtual void Draw() = 0;

9: private:

10: };

Результат:

(1)Circle (2)Rectangle (3)Square (0)Quit: 2

x x x x x x

x x x x x x

x x x x x x

x x x x x x

(1)Circle (2)Rectangle (3)Square (0)Quit: 3

x x x x x

x x x x x

x x x x x

x x x x x

x x x x x

(1)Circle (2)Rectangle (3)Square (0)Quit: 0

Анализ: Как видите, выполнение программы не изменилось. Просто теперь в программе невозможно создать объект класса Shape.

Абстрактные типы данных

Чтобы объявить класс как абстрактный тип данных.достаточно добавить в него одну или несколько чистых виртуальных функций. Для этогопосле объявления функции необходимо добавить - 0, например:

сlass Shape

{

virtual void Draw() = 0; // чистая виртуальная функция

}

Наши рекомендации