
- •Часть I. Традиционные Языки Программирования
- •Глава 1. Управляющие структуры. Процедурные абстракции.
- •Базисные свойства языков программирования
- •Процедурные абстракции
- •Передача управления
- •Передача данных
- •Глава II. Основные понятия и проблемы, связанные с типами данных
- •Глава III. Базисные типы данных
- •3.1 Простые типы данных
- •3.1.2. Другие базисные типы данных.
- •Определение новых типов данных
- •Конструкторы.
- •Конструктор умолчания.
- •Конструктор копирования.
- •Конструктор преобразования.
- •Операторы преобразования.
- •Деструкторы
- •Глава 5. Инкапсуляция. Абстрактные типы данных (атд).
- •Модула–2.
- •Оберон.
- •Инициализация статических членов в Java (отступление)
- •Глава 5. Инкапсуляция. Абстрактные типы данных (атд) (продолжение).
- •Язык Java.
- •Глава 6. Раздельная трансляция.
- •Раздельная независимая трансляция
- •Именование
- •Include-файлы
- •Раздельная зависимая трансляция
- •Глава 7. Статическая параметризация.
- •Язык Ада.
- •Глава 7. Исключительные ситуации в языках программирования.
- •1. Объявление исключений.
- •2. Определение исключения.
- •3. Возникновение.
- •4. Распространение и обработка.
- •Часть II. Объектно-ориентированные яп.
- •Глава I. Наследование в яп.
- •1. Каждый объект данных имеет ровно один тип.
- •2. Типы эквивалентны тогда и только тогда, когда их имена совпадают.
- •3. Каждый тип данных характеризуется набором данных и множеством операций.
- •4. Различные типы не совместимы по присваиванию и передаче параметров.
- •Множественное наследование.
- •Глава 2. Динамическое связывание методов.
- •Динамическое связывание методов
- •Снятие механизма виртуального вызова
- •Абстрактные методы. Абстрактные классы.
- •Динамическая идентификация типа.
- •3. Полиморфизм.
Множественное наследование.
Множественное наследование до сих пор вызывает споры. Наиболее строгие апологеты объектно-ориентированного подхода считают, что множественное наследование – несколько искусственная форма, и что от нее нужно отказаться. Поклонники языка Smalltalk считают, что если чего-то нет в Smalltalk, то это не имеет никакого отношения к объектно-ориентированному (в Smalltalk нет множественного наследования). Наиболее максимальные языки, такие как CLOS (Command Lisp Object System) и С++, используют множественное наследование, хотя именно критика множественного наследования в С++ и послужила катализатором мнений по этому поводу. Сейчас ситуация с множественным наследованием прояснилась окончательно, и тот вариант множественного наследования, который реализован в языке Java в настоящий момент признан оптимальным.
Такие языки, как Delphi, Оберон, Smaltalk множественного наследования не поддерживают.
П
ри
множественном наследовании появляется
возможность моделировать более сложные
отношения между классами, которые в
математике называются решетками. Можно
создавать структуры разных типов.
Подобного рода структуры реализуются следующим образом:
class X {
int i;
}
class Z {
int j;
}
class Y: public X {
int k;
}
class W: public X, public Y, public Z {
int l;
}
С
интаксис
множественного наследования очень
похож на единичное наследование.
Спецификатор public (и другие
спецификаторы) имеет ту же семантику,
что и при единичном наследовании. Объект
класса W обладает полями
классов X, Y,
Z. Кроме того, содержит
свои поля.
К
аким
образом будет распределяться память
под объект? Мы уже говорили, что есть
два подхода распределения памяти: цепной
и линейный. Для множественного наследования
в С++ более эффективен линейный подход.
При цепном подходе, объект класса-наследника
должен содержать не просто ссылку на
базовый класс, а целую таблицу ссылок.
Хотя и линейная схема распределения
памяти в С++ дает некоторые накладные
расходы. Большинство компиляторов
разместит объекты друг за другом.
В объекте класса W будет находиться два экземпляра класса Х. При этом возникает конфликт имен между классами Х и XY. Явным образом указать класс (Х или XY), к которому мы хотим обратиться, в данном случае нельзя, потому что запись X::i не определяет, к какому из двух классов X необходимо обратиться. Компилятор, в данном случае, выбирает правило доминирования имен: имя A::i доминирует над именем B::i в том случае, если A содержит класс B в качестве одного из своих базовых классов. Это правило доминирования будет действовать и при выборе виртуальных функций.
В данном случае, X::i будет означать обращение к верхнему в схеме объекту Х (т.е. к прямому предку). К полю объекта XY можно обратиться и через класс Y (Y::i), но только если это поле в классе Y не переопределено. Иначе к полю объекта XY обратиться невозможно.
Невозможно реализовать следующий вариант наследования:
class X: public Y, public Y{…}
В данном случае было бы невозможно из класса Х обратиться к какому-либо полю Y, потому что оба предка равноправны. Можно было бы специально для этого ввести механизм различения таких классов, но Страуструп избегал такого рода подходов.
Как реализовать ромбовидную схему наследования? Каким образом сделать так, чтобы классы Y и Z были наследниками одного экземпляра объекта Х? Определение класса W выглядит следующим образом:
c
lass
W: public Y, public Z{…}; //определение класса W
ничем не отличается
//классы Y и Z должны быть определены иначе.
class Y: virtual public X{…};
class Z: virtual public X{…};
Классы Y и Z должны наследовать класс Х виртуальным образом. Если один из этих классов наследует не виртуальным образом, то ромбовидная структура не получиться, и будет два экземпляра Х.
Если затем, например, описать класс А следующим образом:
class A: public Y, public Z, public X {…};
то объект этого класса будет содержать два тела Х, одно из которых совместно используется классами Y и Z. Если же мы хотим, чтобы было только одно тело Х, то нужно писать иначе:
class A: public Y, public Z, virtual public X {…};
В каких случаях удобно использовать множественное наследование? Для первой схемы рисунка (левая часть неравенства) можно привести следующий пример. Представим себе, что Х – это обобщенный контейнерный класс, например, линейный список. У контейнерного класса есть набор функций для работы с ним. Для того, чтобы хранить что-то в соответствующем списке, мы должны были добавлять к этим функциям конкретные данные, наследуя контейнерный класс. В данном случае, наличие двух объектов Х означает, что мы хотим, чтобы класс W находился сразу в двух линейных списках (например, список графических объектов на экране и список объектов, которые в данный момент не должны рисоваться).
З
ачем
нужна ромбовидная форма наследования?
Эта форма имеет несколько иную карту
распределения памяти. Классы Y
и Z совместно используют
одно тело X. В стандартной
библиотеке С++ есть пример такой схемы
наследования, потому что, когда Страуструп
вводил некоторые абстракции, он должен
был их оправдать на примере стандартной
библиотеки.
В данном случае, имеется в виду библиотека надежного ввода-вывода iostream. Есть некоторый базовый тип stream (аналог Х), из которого наследуются типы istream и ostream (аналогично классам Y и Z), которые являются базовыми классами для типа iostream (соответственно классу W). Получается ромбовидная схема наследования. Класс stream содержит в себе некий буфер, файловый дескриптор и др. Классы istream и ostream содержат интерфейс для ввода и вывода соответственно. Класс iostream применяется и для ввода и для вывода, причем через один и тот же буфер и один файловый дескриптор. Если бы для чтения и для записи использовались бы разные буфера, то ввод-вывод был бы некорректен.
На первый взгляд, множественное наследование добавляет некоторые преимущества, потому что что-то можно делать, чего раньше делать было нельзя. Хотя все это можно моделировать и с помощью единичного наследования, используя отношение включения. На первый взгляд, эффективная реализация множественного наследования сложности не представляет, но на самом деле существует одна большая проблема, связанная с виртуальными функциями. Множественное наследование было добавлено в С++ в районе 85-86го года, и тогда перед Страуструпом стояла проблема: нужно было срочно довести язык до квазипромышленной реализации, компилятор которой, можно было уже лицензировать. И на него оказывалось менеджерское давление. Перед Страуструпом стояло два вопроса: работать ли над множественным наследованием или работать над шаблонами. Страуструп говорит, что самая большая ошибка, которую он допустил при работе над языком С++, это то, что он занялся реализацией множественного наследования в ущерб статической параметризации.
Проблема здесь не столько в спорном характере множественного наследования, сколько в том, что было упущено время, следующая версия вышла только через четыре года. Если бы шаблоны были бы внедрены в язык в 86-ом году, то к 90-му году уже появилась бы работоспособная и стандартная библиотека шаблонов. Заметьте, что сейчас ситуация такова, что каждая фирма-производитель выпускает свою шаблонную библиотеку. У MFC один набор шаблонов, у Borland другой набор шаблонов. Правда в середине 90-х годов появилась таки библиотека STL (Standard Template Library), однако она появилась, когда поезд уже ушел, и программисты уже не хотят переучиваться.
Язык Java использует множественное наследование только для специальных видов классов, а именно, для классов, которые состоят только из функций. На первый взгляд, кажется, что такие классы смысла не имеют. Возникает вопрос: если нет данных, то с чем же будут работать функции этого класса? Но если это статические функции, то такой класс приобретает смысл модуля. Наследование такого модуля полезно, когда соответствующие функции становятся динамически связываемыми (см. пример на Обероне с рисованием графических объектов).
На этом мы пока закончим тему множественного наследования, чтобы потом к ней вернуться, рассматривая абстрактные классы языка С++ и интерфейсы языка Java. Именно для этих целей и требуется множественное наследование.