НА ГЛАВНУЮ
Меню сайта
Категория
Ghost++ [1]
С++ [55]
Развлечение
ON - LINE
Опрос
Какой артефакт рулит?
Всего ответов: 460
Оbserver Ward

Онлайн всего: 1
Гостей: 1
Пользователей: 0


Друзья сайта
Заведи себе Бота
Hаша кнопка
Для обмена банерами , наша кнопка для размещения у вас на сайте

Клансайт USSR


Главная » Статьи » Программирование » С++

18. Множественное и виртуальное наследование (2)
18.4. Область видимости класса и наследование

У каждого класса есть собственная область видимости, в которой определены имена членов и вложенные типы (см. разделы 13.9 и 13.10). При наследовании область видимости производного класса вкладывается в область видимости непосредственного базового. Если имя не удается разрешить в области видимости производного класса, то поиск определения продолжается в области видимости базового.

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

class ZooAnimal {
public:
   ostream &print( ostream& ) const;

   // сделаны открытыми только ради демонстрации разных случаев
   string is_a;
   int    ival;
private:
   double dval;
};
и упрощенное определение производного класса Bear:
class Bear : public ZooAnimal {
public:
   ostream &print( ostream& ) const;

   // сделаны открытыми только ради демонстрации разных случаев
   string name;
   int    ival;
};

Когда мы пишем:

Bear bear;
bear.is_a;

то имя разрешается следующим образом:

    * bear – это объект класса Bear. Сначала поиск имени is_a ведется в области видимости Bear. Там его нет.
    * Поскольку класс Bear производный от ZooAnimal, то далее поиск is_a ведется в области видимости последнего. Обнаруживается, что имя принадлежит его члену. Разрешение закончилось успешно.

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

bear.ival;

ival – это член класса Bear, найденный на первом шаге описанного выше процесса разрешения имени.

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

bear.ZooAnimal::ival;

Тем самым мы говорим компилятору, что объявление ival следует искать в области видимости класса ZooAnimal.

Проиллюстрируем использование оператора разрешения области видимости на несколько абсурдном примере (надеемся, вы никогда не напишете чего-либо подобного в реальном коде):

int ival;

int Bear::mumble( int ival )
{
   return ival +        // обращение к параметру
        ::ival +        // обращение к глобальному объекту
        ZooAnimal::ival +
        Bear::ival;
}

Неквалифицированное обращение к ival разрешается в пользу формального параметра. (Если бы переменная ival не была определена внутри mumble(), то имел бы место доступ к члену класса Bear. Если бы ival не была определена и в Bear, то подразумевался бы член ZooAnimal. А если бы ival не было и там, то речь шла бы о глобальном объекте.)

Разрешение имени члена класса всегда предшествует выяснению того, является ли обращение к нему корректным. На первый взгляд, это противоречит интуиции. Например, изменим реализацию mumble():

int dval;
int Bear::mumble( int ival )
{
   // ошибка: разрешается в пользу закрытого члена ZooAnimal::dval
   return ival + dval;
}

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

(a)    Определено ли dval в локальной области видимости функции-члена класса Bear? Нет.
     (b)    Определено ли dval в области видимости Bear? Нет.
     (c)    Определено ли dval в области видимости ZooAnimal? Да. Обращение разрешается в пользу этого имени.

После того как имя разрешено, компилятор проверяет, возможен ли доступ к нему. В данном случае нет: dval является закрытым членом, и прямое обращение к нему из mumble() запрещено. Правильное (и, возможно, имевшееся в виду) разрешение требует явного употребления оператора разрешения области видимости:

return ival + ::dval;  // правильно

Почему же имя члена разрешается перед проверкой уровня доступа? Чтобы предотвратить тонкие изменения семантики программы в связи с совершенно независимым, казалось бы, изменением уровня доступа к члену. Рассмотрим, например, такой вызов:

int dval;
int Bear::mumble( int ival )
{
   foo( dval );
   // ...
}

Если бы функция foo() была перегруженной, то перемещение члена ZooAnimal::dval из закрытой секции в защищенную вполне могло бы изменить всю последовательность вызовов внутри mumble(), а разработчик об этом даже и не подозревал бы.

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

ostream& Bear::print( ostream &os) const
{
   // вызывается ZooAnimal::print(os)
   ZooAnimal::print( os );

   os << name;
   return os;
}

18.4.1. Область видимости класса при множественном наследовании

Как влияет множественное наследование на алгоритм просмотра области видимости класса? Все непосредственные базовые классы просматриваются одновременно, что может приводить к неоднозначности в случае, когда в нескольких из них есть одноименные члены. Рассмотрим на нескольких примерах, как возникает неоднозначность и какие меры можно предпринять для ее устранения. Предположим, есть следующий набор классов:

class Endangered {
public:
   ostream& print( ostream& ) const;
   void highlight();
   // ...
};

class ZooAnimal {
public:
   bool onExhibit() const;
   // ...
private:
   bool highlight( int zoo_location );
   // ...
};

class Bear : public ZooAnimal {
public:
   ostream& print( ostream& ) const;
   void dance( dance_type ) const;
   // ...
};

Panda объявляется производным от двух классов:

class Panda : public Bear, public Endangered {
public:
   void cuddle() const;
   // ...
};

Хотя при наследовании функций print() и highlight() из обоих базовых классов Bear и Endangered имеется потенциальная неоднозначность, сообщение об ошибке не выдается до момента явно неоднозначного обращения к любой из этих функций.

В то время как неоднозначность двух унаследованных функций print() очевидна с первого взгляда, наличие конфликта между членами highlight() удивляет (ради этого пример и составлялся): ведь у них разные уровни доступа и разные прототипы. Более того, экземпляр из Endangered – это член непосредственного базового класса, а из ZooAnimal – член класса, стоящего на две ступеньки выше в иерархии.

Однако все это не имеет значения (впрочем, как мы скоро увидим, может иметь, но в случае виртуального наследования). Bear наследует закрытую функцию-член highlight() из ZooAnimal; лексически она видна, хотя вызывать ее из Bear или Panda запрещено. Значит, Panda наследует два лексически видимых члена с именем highlight, поэтому любое неквалифицированное обращение к этому имени приводит к ошибке компиляции.

Поиск имени начинается в ближайшей области видимости, объемлющей его вхождение. Например, в коде

int main()
{
   Panda yin_yang;
   yin_yang.dance( Bear::macarena );
}

ближайшей будет область видимости класса Panda, к которому принадлежит yin_yang. Если же мы напишем:

void Panda::mumble()
{
   dance( Bear::macarena );
   // ...
}

то ближайшей будет локальная область видимости функции-члена mumble(). Если объявление dance в ней имеется, то разрешение имени на этом благополучно завершится. В противном случае поиск будет продолжен в объемлющих областях видимости.

В случае множественного наследования имитируется одновременный просмотр всех поддеревьев наследования – в нашем случае это класс Endangered и поддерево Bear/ZooAnimal. Если объявление обнаружено только в поддереве одного из базовых классов, то разрешение имени заканчивается успешно, как, например, при таком вызове dance():

// правильно: Bear::dance()
yin_yang.dance( Bear::macarena );

Если же объявление найдено в двух или более поддеревьях, то обращение считается неоднозначным и компилятор выдает сообщение об ошибке. Так будет при неквалифицированном обращении к print():

int main()
{
   // ошибка: неоднозначность: одна из
   //         Bear::print( ostream& ) const
   //         Endangered::print( ostream& ) const
   Panda yin_yang;
   yin_yang.print( cout );
}

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

int main()
{
   // правильно, но не лучшее решение
   Panda yin_yang;
   yin_yang.Bear::print( cout );
}

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

inline void Panda::highlight() {
   Endangered::highlight();
}

inline ostream&
Panda::print( ostream &os ) const
{
   Bear::print( os );
   Endangered::print( os );
   return os;
}

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

Упражнение 18.9

Дана следующая иерархия классов:

class Base1 {
public:
   // ...
protected:
   int    ival;
   double dval;
   char   cval;
   // ...
private:
   int    *id;
   // ...
};

class Base2 {
public:
   // ...
protected:
   float fval;
   // ...
private:
   double dval;
   // ...
};

class Derived : public Base1 {
public:
   // ...
protected:
   string sval;
   double dval;
   // ...
};

class MI : public Derived, public Base2 {
public:
   // ...
protected:
   int             *ival;
   complex<double> cval;
   // ...
};
и структура функции-члена MI::foo():
int ival;
double dval;

void MI::
foo( double dval )
{
   int id;
   // ...
}

(a)    Какие члены видны в классе MI? Есть ли среди них такие, которые видны в нескольких базовых?
     (b)    Какие члены видны в MI::foo()?

Упражнение 18.10

Пользуясь иерархией классов из упражнения 18.9, укажите, какие из следующих присваиваний недопустимы внутри функции-члена MI::bar():

void MI::
bar()
{
   int sval;
   // вопрос упражнения относится к коду, начинающемуся с этого места ...
}

(a) dval = 3.14159; (d) fval = 0;
(b) cval = 'a';     (e) sval = *ival;
(c) id = 1;

Упражнение 18.11

Даны иерархия классов из упражнения 18.9 и скелет функции-члена MI::foobar():

int id;
void MI::
foobar( float cval )
{
   int dval;
   // вопросы упражнения относятся к коду, начинающемуся с этого места ...
}
(a)    Присвойте локальной переменной dval сумму значений члена dval класса Base1 и члена dval класса Derived.
(b)    Присвойте вещественную часть члена cval класса MI члену fval класса Base2.
(c)    Присвойте значение члена cval класса Base1 первому символу члена sval класса Derived.

Упражнение 18.12

Дана следующая иерархия классов, в которых имеются функции-члены print():

class Base {
public:
   void print( string ) const;
   // ...
};

class Derived1 : public Base {
public:
   void print( int ) const;
   // ...
};

class Derived2 : public Base {
public:
   void print( double ) const;
   // ...
};

class MI : public Derived1, public Derived2 {
public:
   void print( complex ) const;
   // ...
};
(a)    Почему приведенный фрагмент дает ошибку компиляции?
    MI mi;
    string dancer( "Nejinsky" );
    mi.print( dancer );
(b)    Как изменить определение MI, чтобы этот фрагмент компилировался и выполнялся правильно?

18.5. Виртуальное наследование A

По умолчанию наследование в C++ является специальной формой композиции по значению. Когда мы пишем:

class Bear : public ZooAnimal { ... };

каждый объект Bear содержит все нестатические данные-члены подобъекта своего базового класса ZooAnimal, а также нестатические члены, объявленные в самом Bear. Аналогично, если производный класс является базовым для какого-то другого:

class PolarBear : public Bear { ... };

то каждый объект PolarBear содержит все нестатические члены, объявленные в PolarBear, Bear и ZooAnimal.

В случае одиночного наследования эта форма композиции по значению, поддерживаемая механизмом наследования, обеспечивает компактное и эффективное представление объекта. Проблемы возникают только при множественном наследовании, когда некоторый базовый класс неоднократно встречается в иерархии наследования. Самый известный реальный пример такого рода – это иерархия классов iostream. Взгляните еще раз на рис. 18.2: istream и ostream наследуют одному и тому абстрактному базовому классу ios, а iostream является производным как от istream, так и от ostream.

class iostream :
   public istream, public ostream { ... };

По умолчанию каждый объект iostream содержит два подобъекта ios: из istream и из ostream. Почему это плохо? С точки зрения эффективности хранение двух копий подобъекта ios – пустая трата памяти, поскольку объекту iostream нужен только один экземпляр. Кроме того, конструктор вызывается для каждого подобъекта. Более серьезной проблемой является неоднозначность, к которой приводит наличие двух экземпляров. Например, любое неквалифицированное обращение к члену класса ios дает ошибку компиляции. Какой экземпляр имеется в виду? Что будет, если классы istream и ostream инициализируют свои подобъекты ios по-разному? Можно ли гарантировать, что в классе iostream используется согласованная пара членов ios? Применяемый по умолчанию механизм композиции по значению не дает таких гарантий.

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

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

class Panda : public Bear,
              public Raccoon, public Endangered { ... };

Наша виртуальная иерархия наследования Panda показана на рис. 18.4: две пунктирные стрелки обозначают виртуальное наследование классов Bear и Raccoon от ZooAnimal, а три сплошные – невиртуальное наследование Panda от Bear, Raccoon и, на всякий случай, от класса Endangered из раздела 18.2.

Рис. 18.4. Иерархия виртуального наследования класса Panda

На данном рисунке показан интуитивно неочевидный аспект виртуального наследования: оно (в нашем случае наследование классов Bear и Raccoon) должно появиться в иерархии раньше, чем в нем возникнет реальная необходимость. Необходимым виртуальное наследование становится только при объявлении класса Panda, но если перед этим базовые классы Bear и Raccoon не наследуют своему базовому виртуально, то проектировщику класса Panda не повезло.

Должны ли мы производить свои базовые классы виртуально просто потому, что где-то ниже в иерархии может потребоваться виртуальное наследование? Нет, это не рекомендуется: снижение производительности и усложнение дальнейшего наследования может оказаться существенным (см. [LIPPMAN96a], где приведены и обсуждаются результаты измерения производительности).

Когда же использовать виртуальное наследование? Чтобы его применение было успешным, иерархия, например библиотека iostream или наше дерево классов Panda, должна проектироваться целиком либо одним человеком, либо коллективом разработчиков.

В общем случае мы не рекомендуем пользоваться виртуальным наследованием, если только оно не решает конкретную проблему проектирования. Однако посмотрим, как все-таки можно его применить.
18.5.1. Объявление виртуального базового класса

Для указания виртуального наследования в объявление базового класса вставляется модификатор virtual. Так, в данном примере ZooAnimal становится виртуальным базовым для Bear и Raccoon:

// взаимное расположение ключевых слов public и virtual
// несущественно
class Bear : public virtual ZooAnimal { ... };
class Raccoon : virtual public ZooAnimal { ... };

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

extern void dance( const Bear* );
extern void rummage( const Raccoon* );

extern ostream&
      operator<<( ostream&, const ZooAnimal& );

int main()
{
   Panda yin_yang;

   dance( &yin_yang );   // правильно
   rummage( &yin_yang ); // правильно
   cout << yin_yang;     // правильно
   // ...
}

Любой класс, который можно задать в качестве базового, разрешается сделать виртуальным, причем он способен содержать все те же элементы, что обычные базовые классы. Так выглядит объявление ZooAnimal:

#include <iostream>
#include <string>

class ZooAnimal;
extern ostream&
      operator<<( ostream&, const ZooAnimal& );
class ZooAnimal {
public:
   ZooAnimal( string name,
              bool onExhibit, string fam_name )
            : _name( name ),
              _onExhibit( onExhibit ), _fam_name( fam_name )
   {}

   virtual ~ZooAnimal();
   virtual ostream& print( ostream& ) const;
   string name() const { return _name; }
   string family_name() const { return _fam_name; }
   // ...

protected:
   bool   _onExhibit;
   string _name;
   string _fam_name;
   // ...
};

К объявлению и реализации непосредственного базового класса при использовании виртуального наследования добавляется ключевое слово virtual. Вот, например, объявление нашего класса Bear:

class Bear : public virtual ZooAnimal {
public:
   enum DanceType {
        two_left_feet, macarena, fandango, waltz };

   Bear( string name, bool onExhibit=true )
       : ZooAnimal( name, onExhibit, "Bear" ),
         _dance( two_left_feet )
   {}

   virtual ostream& print( ostream& ) const;
   void dance( DanceType );
   // ...

protected:
   DanceType _dance;
   // ...
};

А вот объявление класса Raccoon:

class Raccoon : public virtual ZooAnimal {
public:
   Raccoon( string name, bool onExhibit=true )
       : ZooAnimal( name, onExhibit, "Raccoon" ),
         _pettable( false )
   {}

   virtual ostream& print( ostream& ) const;

   bool pettable() const { return _pettable; }
   void pettable( bool petval ) { _pettable = petval; }
   // ...

protected:
   bool _pettable;
   // ...
};

18.5.2. Специальная семантика инициализации

Наследование, в котором присутствует один или несколько виртуальных базовых классов, требует специальной семантики инициализации. Взгляните еще раз на реализации Bear и Raccoon в предыдущем разделе. Видите ли вы, какая проблема связана с порождением класса Panda?

class Panda : public Bear,
              public Raccoon, public Endangered {
public:
      Panda( string name, bool onExhibit=true );
      virtual ostream& print( ostream& ) const;

      bool sleeping() const { return _sleeping; }
      void sleeping( bool newval ) { _sleeping = newval; }
      // ...

protected:
      bool _sleeping;
      // ...
};

Проблема в том, что конструкторы базовых классов Bear и Raccoon вызывают конструктор ZooAnimal с неявным набором аргументов. Хуже того, в нашем примере значения по умолчанию для аргумента fam_name (название семейства) не только отличаются, они еще и неверны для Panda.

В случае невиртуального наследования производный класс способен явно инициализировать только свои непосредственные базовые классы (см. раздел 17.4). Так, классу Panda, наследующему от ZooAnimal, не разрешается напрямую вызвать конструктор ZooAnimal в своем списке инициализации членов. Однако при виртуальном наследовании только Panda может напрямую вызывать конструктор своего виртуального базового класса ZooAnimal.

Ответственность за инициализацию виртуального базового возлагается на ближайший производный класс. Например, когда объявляется объект класса Bear:

Bear winnie( "pooh" );

то Bear является ближайшим производным классом для объекта winnie, поэтому выполняется вызов конструктора ZooAnimal, определенный в классе Bear. Когда мы пишем:

cout << winnie.family_name();

будет выведена строка:

The family name for pooh is Bear

(Название семейства для pooh – это Bear)

Аналогично для объявления

Raccoon meeko( "meeko");

Raccoon – это ближайший производный класс для объекта meeko, поэтому выполняется вызов конструктора ZooAnimal, определенный в классе Raccoon. Когда мы пишем:

cout << meeko.family_name();

печатается строка:

The family name for meeko is Raccoon

(Название семейства для meeko - это Raccoon)

Если же объявить объект типа Panda:

Panda yolo( "yolo" );

то ближайшим производным классом для объекта yolo будет Panda, поэтому он и отвечает за инициализацию ZooAnimal.

Когда инициализируется объект Panda, то явные вызовы конструктора ZooAnimal в конструкторах классов Raccoon и Bear не выполняются, а вызывается он с теми аргументами, которые указаны в списке инициализации членов объекта Panda. Вот так выглядит реализация:

Panda::Panda( string name, bool onExhibit=true )
          : ZooAnimal( name, onExhibit, "Panda" ),
            Bear( name, onExhibit ),
            Raccoon( name, onExhibit ),
            Endangered( Endangered::environment,
                        Endangered::critical ),
            sleeping( false )
{}

Если в конструкторе Panda аргументы для конструктора ZooAnimal не указаны явно, то вызывается конструктор ZooAnimal по умолчанию либо, если такового нет, выдается ошибка при компиляции определения конструктора Panda.

Когда мы пишем:

cout << yolo.family_name();

печатается строка:

The family name for yolo is Panda

(Название семейства для yolo - это Panda)

Внутри определения Panda классы Raccoon и Bear являются промежуточными, а не ближайшими производными. В промежуточном производном классе все прямые вызовы конструкторов виртуальных базовых классов автоматически подавляются. Если бы от Panda был в дальнейшем произведен еще один класс, то сам класс Panda стал бы промежуточным и вызов из него конструктора ZooAnimal также был бы подавлен.

Обратите внимание, что оба аргумента, передаваемые конструкторам Bear и Raccoon, излишни в том случае, когда они выступают в роли промежуточных производных классов. Чтобы избежать передачи ненужных аргументов, мы можем предоставить явный конструктор, вызываемый, когда класс оказывается промежуточным производным. Изменим наш конструктор Bear:

class Bear : public virtual ZooAnimal {
public:
   // если выступает в роли ближайшего производного класса
   Bear( string name, bool onExhibit=true )
       : ZooAnimal( name, onExhibit, "Bear" ),
         _dance( two_left_feet )
   {}

   // ... остальное без изменения

protected:
   // если выступает в роли промежуточного производного класса
   Bear() : _dance( two_left_feet ) {}

   // ... остальное без изменения
};

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

Panda::Panda( string name, bool onExhibit=true )
          : ZooAnimal( name, onExhibit, "Panda"),
            Endangered( Endangered::environment,
                        Endangered::critical ),
            sleeping( false )
{}

18.5.3. Порядок вызова конструкторов и деструкторов

Виртуальные базовые классы всегда конструируются перед невиртуальными, вне зависимости от их расположения в иерархии наследования. Например, в приведенной иерархии у класса TeddyBear (плюшевый мишка) есть два виртуальных базовых: непосредственный – ToyAnimal (игрушечное животное) и экземпляр ZooAnimal, от которого унаследован класс Bear:

class Character { ... };         // персонаж
class BookCharacter : public Character { ... };
                                 // литературный персонаж
class ToyAnimal { ... };         // игрушка

class TeddyBear : public BookCharacter,
                  public Bear, public virtual ToyAnimal
                  { ... };

Эта иерархия изображена на рис. 18.5, где виртуальное наследование показано пунктирной стрелкой, а невиртуальное – сплошной.

Рис. 18.5. Иерархия виртуального наследования класса TeddyBear

Непосредственные базовые классы просматриваются в порядке их объявления при поиске среди них виртуальных. В нашем примере сначала анализируется поддерево наследования BookCharacter, затем Bear и наконец ToyAnimal. Каждое поддерево обходится в глубину, т.е. поиск начинается с корневого класса и продвигается вниз. Так, для поддерева BookCharacter сначала просматривается Character, а затем BookCharacter. Для поддерева Bear – ZooAnimal, а потом Bear.

При описанном алгоритме поиска порядок вызова конструкторов виртуальных базовых классов для TeddyBear таков: ZooAnimal, потом ToyAnimal.

После того как вызваны конструкторы виртуальных базовых классов , настает черед конструкторов невиртуальных, которые вызываются в порядке объявления: BookCharacter, затем Bear. Перед выполнением конструктора BookCharacter вызывается конструктор его базового класса Character.

Если имеется объявление:

TeddyBear Paddington;

то последовательность вызова конструкторов базовых классов будет такой:

ZooAnimal();          // виртуальный базовый класс Bear
      ToyAnimal();          // непосредственный виртуальный базовый класс
      Character();          // невиртуальный базовый класс BookCharacter
      BookCharacter();      // непосредственный невиртуальный базовый класс
      Bear();               // непосредственный невиртуальный базовый класс
      TeddyBear();          // ближайший производный класс

причем за инициализацию ZooAnimal и ToyAnimal отвечает TeddyBear – ближайший производный класс объекта Paddington.

Порядок вызова копирующих конструкторов при почленной инициализации (и копирующих операторов присваивания при почленном присваивании) такой же. Гарантируется, что деструкторы вызываются в последовательности, обратной вызову конструкторов.
18.5.4. Видимость членов виртуального базового класса

Изменим наш класс Bear так, чтобы он имел собственную реализацию функции-члена onExhibit(), предоставляемой также ZooAnimal: bool Bear::onExhibit() { ... }

Теперь обращение к onExhibit() через объект Bear разрешается в пользу экземпляра, определенного в этом классе:

Bear winnie( "любитель меда" );
winnie.onExhibit();       // Bear::onExhibit()

Обращение же к onExhibit() через объект Raccoon разрешается в пользу функции-члена, унаследованной из ZooAnimal:

Raccoon meeko( "любитель всякой еды" );
meeko.onExhibit();       // ZooAnimal::onExhibit()

Производный класс Panda наследует члены своих базовых классов. Их можно отнести к одной из трех категорий:

    * члены виртуального базового класса ZooAnimal, такие, как name() и family(), не замещенные ни в Bear, ни в Raccoon;
    * член onExhibit() виртуального базового класса ZooAnimal, наследуемый при обращении через Raccoon и замещенный в классе Bear;
    * специализированные в классах Bear и Raccoon экземпляры функции print() из ZooAnimal.

Можно ли, не опасаясь неоднозначности, напрямую обращаться к унаследованным членам из области видимости класса Panda? В случае невиртуального наследования – нет: все неквалифицированные ссылки на имя неоднозначны. Что касается виртуального наследования, то прямое обращение допустимо к любым членам из первой и второй категорий. Например, дан объект класса Panda:

Panda spot( "Spottie" );

Тогда инструкция

spot.name();

вызывает разделяемую функцию-член name() виртуального базового ZooAnimal, а инструкция

spot.onExhibit();

вызывает функцию-член onExhibit() производного класса Bear.

Когда два или более экземпляров члена наследуются разными путями (это относится не только к функциям-членам, но и к данным-членам, а также к вложенным типам) и все они представляют один и тот же член виртуального базового класса, неоднозначности не возникает, поскольку существует единственный разделяемый экземпляр (первая категория). Если один экземпляр представляет член виртуального базового, а другой – член унаследованного от него класса, то неоднозначности также не возникает: специализированному экземпляру из производного класса отдается предпочтение по сравнению с разделяемым экземпляром из виртуального базового (вторая категория). Но если оба экземпляра представляют члены производных классов, то прямое обращение неоднозначно. Лучше всего разрешить эту ситуацию, предоставив замещающий экземпляр в производном классе (третья категория).

Например, при невиртуальном наследовании неквалифицированное обращение к onExhibit() через объект Panda неоднозначно:

// ошибка: неоднозначно при невиртуальном наследовании
Panda yolo( "любитель бамбука" );
yolo.onExhibit();

В данном случае все унаследованные экземпляры имеют равные приоритеты при разрешении имени, поэтому неквалифицированное обращение приводит к ошибке компиляции из-за неоднозначности (см. раздел 18.4.1).

При виртуальном наследовании члену, унаследованному из виртуального базового класса, приписывается меньший приоритет, чем члену с тем же именем, замещенному в производном. Так, унаследованному от Bear экземпляру onExhibit() отдается предпочтение перед экземпляром из ZooAnimal, унаследованному через Raccoon:

// правильно: при виртуальном наследовании неоднозначности нет
// вызывается Bear::onExhibit()
yolo.onExhibit();

Если два или более классов на одном и том же уровне наследования замещают некоторый член виртуального базового, то в производном они будут иметь одинаковый вес. Например, если в Raccoon также определен член onExhibit(), то при обращении к нему из Panda придется квалифицировать имя с помощью оператора разрешения области видимости:

bool Panda::onExhibit()
{
   return Bear::onExhibit() &&
          Raccoon::onExhibit() &&
          ! _sleeping;
}

Упражнение 18.13

Дана иерархия классов:

class Class { ... };
class Base : public Class { ... };
class Derived1 : virtual public Base { ... };
class Derived2 : virtual public Base { ... };
class MI : public Derived1,
           public Derived2 { ... };
class Final : public MI, public Class { ... };
(a)    В каком порядке вызываются конструкторы и деструкторы при определении объекта Final?
(b)    Сколько подобъектов класса Base содержит объект Final? А сколько подобъектов Class?
(c)    Какие из следующих присваиваний вызывают ошибку компиляции?
Base     *pb;
MI       *pmi;
Class    *pc;
Derived2 *pd2;

(i)  pb = new Class;   (iii) pmi = pb;
(ii) pc = new Final;   (iv)  pd2 = pmi;

Упражнение 18.14

Дана иерархия классов:

class Base {
public:
   bar( int );
   // ...
protected:
   int ival;
   // ...
};

class Derived1 : virtual public Base {
public:
   bar( char );
   foo( char );
   // ...
protected:
   char cval;
   // ...
};

class Derived2 : virtual public Base {
public:
   foo( int );
   // ...
protected:
   int ival;
   char cval;
   // ...
};

class VMI : public Derived1, public Derived2 {};

К каким из унаследованных членов можно обращаться из класса VMI, не квалифицируя имя? А какие требуют квалификации?

Упражнение 18.15

Дан класс Base с тремя конструкторами:

class Base {
public:
   Base();
   Base( string );
   Base( const Base& );
   // ...
protected:
   string _name;
};

Определите соответствующие конструкторы для каждого из следующих классов:

(a) любой из
          class Derived1 : virtual public Vase { ... };
          class Derived2 : virtual public Vase { ... };
      (b) class VMI : public Derived1, public Derived2 { ... };
      (c) class Final : public VMI { ... };
Категория: С++ | Добавил: r2d2 (29.09.2011)
Просмотров: 1561 | Рейтинг: 0.0/0
Всего комментариев: 0
Добавлять комментарии могут только зарегистрированные пользователи.
[ Регистрация | Вход ]
Born in Ussr
Залогиниться
Турниры

/j clan ussr /j clan cccp