7.4.
Множественное наследование в C++
Так
же, как язык CLOS представляет собой объектно-ориентированное расширение языка
LISP, так и язык C++ создан на основе широко известного языка С и сохранил все
его возможности, добавив к ним средства объектно-ориентированного программирования.
Если отвлечься от того факта, что CLOS и C++ основаны на разных языках-прототипах,
то основное отличие между ними заключается в реализации механизма наследования,
в частности множественного наследования. В языке C++ множественное наследование
трактуется совсем не так, как мы это делали в предшествующих разделах настоящей
главы, а потому этот вопрос заслуживает подробного обсуждения.
В языке C++ родовые операции
реализуются в виде виртуальных функций. Виртуальная функция, объявленная
в классе X, это функция, которая может быть перегружена (переопределена)
в классе, производном от X. При объявлении в классе X виртуальная функция вообще
может не иметь тела, т.е. программного кода реализации. В таком случае
функция называется чисто виртуальной, а класс, имеющий одну или более чисто
виртуальных функций, является абстрактным базовым классом, экземпляры которого
создать невозможно. В любом случае ключевое слово virtual говорит компилятору,
что программный код функции будет уточнен в производных классах.
Те методы, которые вызываются
на выполнение, являются невиртуальными членами-функциями, т.е. функциями, имеющими
определенный программный код, который не перегружается в производных классах.
В этом смысле C++ существенно отличается от языка CLOS, в котором практически
все функции суперкласса в большей или меньшей степени модифицируются механизмом
наложения методов. Поэтому в C++ существует множество синтаксических тонкостей,
в которых не нуждается CLOS. Например, во всех классах иерархии виртуальная функция
должна иметь квалификатор virtual до тех пор, пока в некотором производном классе
не будет представлена ее конкретная реализация.
В чисто иерархической структуре
классов, когда каждый производный класс имеет единственного "родителя",
передача методов по наследству выполняется совершенно очевидным способом. Единственная
тонкость в реализации этого механизма в C++ состоит в использовании квалификаторов
наследования public и private. Если не вдаваться в подробности, то наследование
вида public представляет собой отношение "is а" (является), которое
мы использовали при обсуждении фреймов. Наследование вида private ближе к отношению
"реализовано в терминах", которое позволяет скрыть определенные детали
реализации интерфейсов объектов. Такое полезное разделение "выпало"
в языке CLOS, в котором каждое отношение "класс-подкласс" несет семантический
смысл.
Но
если обратиться к множественному наследованию, то механизмы его реализации в C++
и CLOS существенно отличаются. Поскольку в языке C++ не существует такого понятия,
как порядок предшествования классов, то даже такой простой случай, как в рассмотренном
выше примере "Алмаз Никсона", приводит к неоднозначности. Будем считать,
что отношения между классами Person, Quaker, Republican и Republican_Quaker, как
и прежде, имеют вид, представленный на рис. 7.8.
Объявление классов Person,
Quaker, Republican и Republican Quaker на языке C++ показано в листинге 7.2 (программный
код объявления включен в файл nixon.h).
Рис.
7.8. Отношения между классами в примере "Алмаз Никсона"
Листинг 7.2. Файл nixon.h.
Объявление классов, версия 1
//
Объявление классов для задачи "Алмаз Никсона" finclude <iostream.h>
class
Person
{
public:
Personf)
{};
virtual
"Person() {};
virtual
void speak() = 0; };
class
Republican : public Person
{
public:
Republican))
{};
virtual
~Republican)) {};
virtual
void speak() { cout « "War";} };
class Quaker : public
Person
{
public:
Quakerf)
{};
virtual
~Quaker)) {};
virtual
void speak)) { cout « "Peace";} };
class Republican_Quaker : public Republican,
public Quaker
{
public:
Republican_Quaker()
{};
virtual
~Republican_Quaker() {};
};
Создадим
экземпляр richard класса Republican_Quaker.
#include
"nixon.h" void main))
Republican_Quaker
richard; richard.speak));
При
обработке этого программного кода компилятор C++ обнаружит, что вызов richard.speak))
содержит неоднозначную ссылку. Оно и понятно, поскольку нельзя однозначно заключить,
скажет ли Ричард "War" (война) или "Peace" (мир).
Если мы решим, что метод speak))
класса Republican_Quaker должен "брать пример" с класса Quaker, то проблему
можно решить, определив этот метод одним из двух способов:
void S::speak(){ cout
<< "Peace"; }
или
void
S::speak)({Quaker::speak)); }
Первый
вариант просто перегружает оба наследованных определения метода, а второй в явном
виде вызывает один из них, а именно тот вариант, который реализован в классе Quaker.
Однако
совершенно незначительное на первый взгляд изменение в файле определения классов
может разительно изменить поведение объекта. Предположим, решено удалить объявления
методов speak)) из всех классов, кроме Person, как это показано в листинге 7.3.
Листинг
7.3. Файл nixon.h. Объявление классов, версия 2
class Person
public:
Person)) {};
virtual "Person))
{};
virtual
void speak)){ cout « "Beer";}
};
class Republican :
public Person
public:
Republican))
{}; virtual ~Republican)) {};
class
Quaker : public Person
public:
Quaker))
{};
virtual
~Quaker)) {};
class
Republican Quaker : public Republican, public Quaker
{
public:
Republican_Quaker(
) {} ;
virtual
~Republican_Quaker( ) {};
}
При
обработке такого файла определения компилятор опять выдаст сообщение о неоднозначности
ссылки на метод speak ( ). Это произойдет по той причине, что компилятор сформирует
две копии объявления класса Person — по одной для каждого пути наследования, а
это приведет к конфликту имен. Чтобы устранить эту неоднозначность, нужно объявить
Person как виртуальный базовый класс и для Republican, и для Quaker. Тогда
оба производных класса будут ссылаться на единственный объект суперкласса (листинг
7.4).
Листинг
7.4. Файл nixon.h. Объявление классов, версия 3
class Person
{ public:
Per son () {};
virtual
"Person)) {};
virtual
void speak(){ cout << "Beer";} И
class Republican :
virtual public Person
{
public:
Republican))
{};
virtual
~Republican)) {};
};
class
Quaker : virtual public Person
{
public:
Quaker))
{};
virtual
~Quaker)) .{};
}
class
Republican_Quaker : public Republican, public Quaker
{
public:
Republican_Quaker { ) { } ;
virtual "Republican_Quaker( ) {};
}
Объявление
Person в качестве виртуального базового класса для Republican и Quaker имеет и
еще одно преимущество. Предположим, что нам нужно сделать так, чтобы класс Republican_Quaker
отдавал предпочтение стилю поведения квакеров, а все другие были индифферентны
к вопросам войны и мира и следовали линии поведения, определенной классом Person.
Тогда, поскольку Person является виртуальным базовым классом, можно заставить
доминировать Quaker::speak)) над Person::speak)) для класса Republican_Quaker
(листинг 7.5).
Листинг
7.5. Файл nixon.h. Объявление классов, версия 4
class Person
public:
Person)) {};
virtual ~Person))
{};
virtual
void speak)){ cout « "Beer";}
class Republican :
virtual public Person
public:
Republican))
{}; virtual ~Republican)) {};
class
Quaker : virtual public Person
public:
Quaker))
{};
virtual
~Quaker() {};
virtual
void speak)) { cout « "Peace";}
class Republican_Quaker
: public Republican, public Quaker
public:
Republican_Quaker()
{};
virtual
"Republican_Quaker() {};
}
При создании языка C++ преследовалась
цель не усложнять механизм множественного наследования по сравнению с единственным
и разрешать все неоднозначности на стадии компиляции [Stromtrup, 1977]. В
этом существенное различие между C++ и SmallTalk. В последнем такого рода конфликты
разрешаются на стадии выполнения программы. Это также отличается и от метода,
основанного на списке предшествования классов, который используется в CLOS. Кроме
того, в языке CLOS конфликта имен, подобного тому, который мы наблюдали с классом
Person, быть просто не может, поскольку все базовые классы с одинаковыми именами
считаются идентичными.
Таким
образом, за высокую эффективность языка C++ приходится платить, тщательно продумывая
передачу свойств и поведения от классов родителей к производным классам с учетом
всех нюансов функционирования механизма наследственности в C++.
В этом отношении C++ напоминает
свой прототип — язык С, который требует гораздо более близкого знакомства с работой
компьютера, чем язык LISP, поскольку позволяет напрямую обращаться к памяти компьютера,
манипулировать адресами, формировать собственный механизм выделения памяти и т.д.
Какую стратегию предпочесть — зависит от индивидуальных предпочтений разработчика,
но если главным требованием к продукту является высокая производительность, то
чем большими возможностями управления ресурсами обладает разработчик, тем лучше,
тем более эффективную программу можно создать.
Суммируя все сказанное о языке
C++, отметим, что он вполне может послужить базовым программным инструментом для
создания экспертных систем. Если потребуется интерпретатор порождающих правил,
то можно либо разработать его самостоятельно (хотя это и далеко не тривиальная
задача), либо воспользоваться одним из имеющихся на рынке, которые допускают внедрение
в среду C++. Если вам удастся избежать описанных выше сложностей в реализации
множественного наследования, вы сможете в полной мере воспользоваться многочисленными
преимуществами этого языка — проверкой статических типов, разделением между закрытым
и общедоступным наследованием, множеством средств защиты данных от случайных изменений.
| Maya 3D графика в кино и телевидении Воздействие испытаний ядерного оружия на здоровье населения Объектно-ориентированный язык программирования Java Объектно-ориентированное программирование Delphi Библиотека визуальных компонентов VCL и ее базовые классы Кроссплатформенное программирование для Linux Элементы управления Win32 Элементы управления Windows XP Файлы и устройства ввода/вывода Что такое экспертная система? Объектно-ориентированное программирование Инструментальные средства разработки экспертных систем Программирование на языке CLIPS Критерии и количественные характеристики надежности Расчет характеристик надежности невостанавливаемых резервированных изделий Расчет надежности системы с постоянным резервированием Интегрирование тригонометрических функций ; |