Язык программирования C++ от Страуструпа


Множественное наследование


Если класс A является базовым классом для B, то B наследует атрибуты A.  т.е. B содержит A плюс еще что-то. С учетом этого становится очевидно, что хорошо, когда класс B может наследовать из двух базовых классов A1 и A2. Это называется множественным наследованием.

Приведем некий типичный пример множественного наследования. Пусть есть два библиотечных класса displayed и task. Первый представляет задачи, информация о которых может выдаваться на экран с помощью некоторого монитора, а второй - задачи, выполняемые под управлением некоторого диспетчера. Программист может создавать собственные классы, например, такие:

class my_displayed_task: public displayed, public task

{

  // текст пользователя

};

class my_task: public task {

  // эта задача не изображается

  // на экране, т.к. не содержит класс displayed

  // текст пользователя

};

class my_displayed: public displayed

{



  // а это не задача

  // т.к. не содержит класс task

  // текст пользователя

};

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

Все неоднозначности выявляются на стадии трансляции:

class task

{

  public:

     void trace ();

     // ...

};

class displayed

{

  public:

     void trace ();

     // ...

};

class my_displayed_task:public displayed, public task

{

  // в этом классе trace () не определяется

};

void g ( my_displayed_task * p )

{

  p -> trace ();  // ошибка: неоднозначность

}

В этом примере видны отличия С++ от объектно-ориентированных диалектов

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

class my_displayed_task:public displayed, public task

{

  // ...

  public:

     void trace ()

     {

       // текст пользователя

       displayed::trace ();  // вызов trace () из displayed

       task::trace ();       // вызов trace () из task

     }

     // ...

};

void g ( my_displayed_task * p )

{

  p -> trace ();  // теперь нормально

}


В $$1.5.3 и $$6.2.3 уже говорилось, что у класса может быть несколько

прямых базовых классов. Это значит, что в описании класса после : может быть указано более одного класса. Рассмотрим задачу моделирования, в которой параллельные действия представлены стандартной библиотекой классов task, а сбор и выдачу информации обеспечивает библиотечный класс displayed. Тогда класс моделируемых объектов (назовем его satellite) можно определить так:

class satellite : public task, public displayed {

  // ...

};

Такое определение обычно называется множественным наследованием. Обратно, существование только одного прямого базового класса называется единственным наследованием.

Ко всем определенным в классе satellite операциям добавляется объединение операций классов task и displayed:

void f(satellite& s)

{

  s.draw();    // displayed::draw()

  s.delay(10); // task::delay()

  s.xmit();    // satellite::xmit()

}

С другой стороны, объект типа satellite можно передавать функциям с параметром типа task или displayed:

void highlight(displayed*);

void suspend(task*);

void g(satellite* p)

{

  highlight(p);   // highlight((displayed*)p)

  suspend(p);     // suspend((task*)p);

}

Очевидно, реализация этой возможности требует некоторого (простого) трюка от транслятора: нужно функциям с параметрами task и displayed передать разные части объекта типа satellite.

Для виртуальных функций, естественно, вызов и так выполнится правильно:

class task {

  // ...

  virtual pending() = 0;

};

class displayed {

  // ...

  virtual void draw() = 0;

};

class satellite : public task, public displayed {

  // ...

  void pending();

  void draw();

};

Здесь функции satellite::draw() и satellite::pending() для объекта типа satellite будут вызываться так же, как если бы он был объектом типа displayed или task, соответственно.

Отметим, что ориентация только на единственное наследование ограничивает возможности реализации классов displayed, task и satellite. В таком случае класс satellite мог бы быть task или displayed, но не то и другое вместе (если, конечно, task не является производным от displayed или наоборот). В любом случае теряется гибкость.



Содержание раздела