Главная страница
Навигация по странице:

  • 18.5.1. Объявление виртуального базового класса

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

  • Язык программирования C++. Вводный курс. С для начинающих


    Скачать 5.41 Mb.
    НазваниеС для начинающих
    Дата24.08.2022
    Размер5.41 Mb.
    Формат файлаpdf
    Имя файлаЯзык программирования C++. Вводный курс.pdf
    ТипДокументы
    #652350
    страница89 из 93
    1   ...   85   86   87   88   89   90   91   92   93

    954
    };
    (a) Почему приведенный фрагмент дает ошибку компиляции? mi.print( dancer );
    (b) Как изменить определение MI, чтобы этот фрагмент компилировался и выполнялся правильно?
    18.5.
    Виртуальное наследование A
    По умолчанию наследование в C++ является специальной формой композиции по значению. Когда мы пишем: class Bear : public ZooAnimal { ... }; каждый объект Bear содержит все нестатические данные-члены подобъекта своего базового класса ZooAnimal, а также нестатические члены, объявленные в самом Bear.
    Аналогично, если производный класс является базовым для какого-то другого: class PolarBear : public Bear { ... }; то каждый объект PolarBear содержит все нестатические члены, объявленные в
    PolarBear
    , Bear и ZooAnimal.
    В случае одиночного наследования эта форма композиции по значению, поддерживаемая механизмом наследования, обеспечивает компактное и эффективное представление объекта. Проблемы возникают только при множественном наследовании, когда некоторый базовый класс неоднократно встречается в иерархии наследования. Самый 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;
    // ...
    MI mi; string dancer( "Nejinsky" );

    С++ для начинающих
    955
    известный реальный пример такого рода – это иерархия классов iostream. Взгляните еще раз на рис. 18.2: istream и ostream наследуют одному и тому абстрактному базовому классу ios, а iostream является производным как от istream, так и от ostream public istream, public ostream { ... };
    По умолчанию каждый объект iostream содержит два подобъекта ios: из istream и из ostream
    . Почему это плохо? С точки зрения эффективности хранение двух копий подобъекта ios – пустая трата памяти, поскольку объекту iostream нужен только один экземпляр. Кроме того, конструктор вызывается для каждого подобъекта. Более серьезной проблемой является неоднозначность, к которой приводит наличие двух экземпляров.
    Например, любое неквалифицированное обращение к члену класса ios дает ошибку компиляции. Какой экземпляр имеется в виду? Что будет, если классы istream и ostream инициализируют свои подобъекты ios по-разному? Можно ли гарантировать, что в классе iostream используется согласованная пара членов ios? Применяемый по умолчанию механизм композиции по значению не дает таких гарантий.
    Для решения данной проблемы язык предоставляет альтернативный механизм композиции по ссылке: виртуальное наследование. В этом случае наследуется только один разделяемый подобъект базового класса, независимо от того, сколько раз базовый класс встречается в иерархии наследования. Этот разделяемый подобъект называется
    виртуальным базовым классом. С помощью виртуального наследования снимаются проблемы дублирования подобъектов базового класса и неоднозначностей, к которым такое дублирование приводит.
    Для изучения синтаксиса и семантики виртуального наследования мы выбрали класс
    Panda
    . В зоологических кругах уже на протяжении ста лет периодически вспыхивают ожесточенные споры по поводу того, к какому семейству относить панду: к медведям или к енотам. Поскольку проектирование программного обеспечения призвано обслуживать, в основном, интересы прикладных областей, то самое правильное – произвести класс
    Panda от обоих классов: public Raccoon, public Endangered { ... };
    Наша виртуальная иерархия наследования Panda показана на рис. 18.4: две пунктирные стрелки обозначают виртуальное наследование классов Bear и Raccoon от ZooAnimal, а три сплошные – невиртуальное наследование Panda от Bear, Raccoon и, на всякий случай, от класса Endangered из раздела 18.2.
    ZooAnimal Endangered
    Bear Raccoon class iostream : class Panda : public Bear,

    С++ для начинающих
    956
    Panda
    > невиртуальное наследование
    - - - -> виртуальное наследование
    Рис. 18.4. Иерархия виртуального наследования класса Panda
    На данном рисунке показан интуитивно неочевидный аспект виртуального наследования: оно (в нашем случае наследование классов Bear и Raccoon) должно появиться в иерархии раньше, чем в нем возникнет реальная необходимость. Необходимым виртуальное наследование становится только при объявлении класса Panda, но если перед этим базовые классы Bear и Raccoon не наследуют своему базовому виртуально, то проектировщику класса Panda не повезло.
    Должны ли мы производить свои базовые классы виртуально просто потому, что где-то ниже в иерархии может потребоваться виртуальное наследование? Нет, это не рекомендуется: снижение производительности и усложнение дальнейшего наследования может оказаться существенным (см. [LIPPMAN96a], где приведены и обсуждаются результаты измерения производительности).
    Когда же использовать виртуальное наследование? Чтобы его применение было успешным, иерархия, например библиотека iostream или наше дерево классов Panda, должна проектироваться целиком либо одним человеком, либо коллективом разработчиков.
    В общем случае мы не рекомендуем пользоваться виртуальным наследованием, если только оно не решает конкретную проблему проектирования. Однако посмотрим, как все- таки можно его применить.
    18.5.1.
    Объявление виртуального базового класса
    Для указания виртуального наследования в объявление базового класса вставляется модификатор virtual. Так, в данном примере ZooAnimal становится виртуальным базовым для Bear и Raccoon: class Raccoon : virtual public ZooAnimal { ... };
    Виртуальное наследование не является явной характеристикой самого базового класса, а лишь описывает его отношение к производному. Как мы уже отмечали, виртуальное наследование – это разновидность композиции по ссылке. Иначе говоря, доступ к подобъекту и его нестатическим членам косвенный, что обеспечивает гибкость, необходимую для объединения нескольких виртуально унаследованных подобъектов базовых классов в один разделяемый экземпляр внутри производного. В то же время объектом производного класса можно манипулировать через указатель или ссылку на тип базового, хотя последний является виртуальным. Например, все показанные ниже
    // взаимное расположение ключевых слов public и virtual
    // несущественно class Bear : public virtual ZooAnimal { ... };

    С++ для начинающих
    957
    преобразования базовых классов Panda выполняются корректно, хотя Panda использует виртуальное наследование:
    }
    Любой класс, который можно задать в качестве базового, разрешается сделать виртуальным, причем он способен содержать все те же элементы, что обычные базовые классы. Так выглядит объявление ZooAnimal:
    };
    К объявлению и реализации непосредственного базового класса при использовании виртуального наследования добавляется ключевое слово virtual. Вот, например, объявление нашего класса Bear: 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; // правильно
    // ...
    #include
    #include 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;
    // ...

    С++ для начинающих
    958
    };
    А вот объявление класса Raccoon:
    };
    18.5.2.
    Специальная семантика инициализации
    Наследование, в котором присутствует один или несколько виртуальных базовых классов, требует специальной семантики инициализации. Взгляните еще раз на реализации Bear и Raccoon в предыдущем разделе. Видите ли вы, какая проблема связана с порождением класса Panda? 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;
    // ... 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;
    // ...

    С++ для начинающих
    959
    };
    Проблема в том, что конструкторы базовых классов 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(); 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;
    // ...

    С++ для начинающих
    960
    печатается строка:
    The family name for meeko is Raccoon
    (Название семейства для meeko - это Raccoon)
    Если же объявить объект типа Panda:
    Panda yolo( "yolo" ); то ближайшим производным классом для объекта yolo будет Panda, поэтому он и отвечает за инициализацию ZooAnimal.
    Когда инициализируется объект Panda, то явные вызовы конструктора ZooAnimal в конструкторах классов Raccoon и Bear не выполняются, а вызывается он с теми аргументами, которые указаны в списке инициализации членов объекта Panda. Вот так выглядит реализация:
    {}
    Если в конструкторе 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:
    Panda::Panda( string name, bool onExhibit=true )
    : ZooAnimal( name, onExhibit, "Panda" ),
    Bear( name, onExhibit ),
    Raccoon( name, onExhibit ),
    Endangered( Endangered::environment,
    Endangered::critical ), sleeping( false )

    С++ для начинающих
    961
    };
    Мы сделали этот конструктор защищенным, поскольку он вызывается только из производных классов. Если аналогичный конструктор по умолчанию обеспечен и для класса Raccoon, можно следующим образом модифицировать конструктор Panda:
    {}
    18.5.3.
    Порядок вызова конструкторов и деструкторов
    Виртуальные базовые классы всегда конструируются перед невиртуальными, вне зависимости от их расположения в иерархии наследования. Например, в приведенной иерархии у класса TeddyBear (плюшевый мишка) есть два виртуальных базовых: непосредственный – ToyAnimal (игрушечное животное) и экземпляр ZooAnimal, от которого унаследован класс Bear:
    { ... };
    Эта иерархия изображена на рис. 18.5, где виртуальное наследование показано пунктирной стрелкой, а невиртуальное – сплошной.
    Character ZooAnimal ToyAnimal 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 ) {}
    // ... остальное без изменения
    Panda::Panda( string name, bool onExhibit=true )
    : ZooAnimal( name, onExhibit, "Panda" ),
    Endangered( Endangered::environment,
    Endangered::critical ), sleeping( false ) class Character { ... }; // персонаж class BookCharacter : public Character { ... };
    // литературный персонаж class ToyAnimal { ... }; // игрушка class TeddyBear : public BookCharacter, public Bear, public virtual ToyAnimal

    С++ для начинающих
    962
    BookCharacter Bear
    TeddyBear
    > невиртуальное наследование
    - - - -> виртуальноe наследование
    Рис. 18.5. Иерархия виртуального наследования класса TeddyBear
    Непосредственные базовые классы просматриваются в порядке их объявления при поиске среди них виртуальных. В нашем примере сначала анализируется поддерево наследования BookCharacter, затем Bear и наконец ToyAnimal. Каждое поддерево обходится в глубину, т.е. поиск начинается с корневого класса и продвигается вниз. Так, для поддерева BookCharacter сначала просматривается Character, а затем
    BookCharacter
    . Для поддерева Bear – ZooAnimal, а потом Bear.
    При описанном алгоритме поиска порядок вызова конструкторов виртуальных базовых классов для TeddyBear таков: ZooAnimal, потом ToyAnimal.
    После того как вызваны конструкторы виртуальных базовых классов , настает черед конструкторов невиртуальных, которые вызываются в порядке объявления:
    BookCharacter
    , затем Bear. Перед выполнением конструктора BookCharacter вызывается конструктор его базового класса Character.
    Если имеется объявление:
    TeddyBear Paddington; то последовательность вызова конструкторов базовых классов будет такой:
    TeddyBear(); // ближайший производный класс причем за инициализацию ZooAnimal и ToyAnimal отвечает TeddyBear – ближайший производный класс объекта Paddington.
    Порядок вызова копирующих конструкторов при почленной инициализации (и копирующих операторов присваивания при почленном присваивании) такой же.
    Гарантируется, что деструкторы вызываются в последовательности, обратной вызову конструкторов.
    ZooAnimal(); // виртуальный базовый класс Bear
    ToyAnimal(); // непосредственный виртуальный базовый класс
    Character(); // невиртуальный базовый класс BookCharacter
    BookCharacter(); // непосредственный невиртуальный базовый класс
    Bear(); // непосредственный невиртуальный базовый класс

    С++ для начинающих
    963
    18.5.4.
    Видимость членов виртуального базового класса
    Изменим наш класс Bear так, чтобы он имел собственную реализацию функции-члена onExhibit()
    , предоставляемой также ZooAnimal: bool Bear::onExhibit() { ... }
    Теперь обращение к onExhibit() через объект Bear разрешается в пользу экземпляра, определенного в этом классе: winnie.onExhibit(); // Bear::onExhibit()
    Обращение же к onExhibit() через объект Raccoon разрешается в пользу функции- члена, унаследованной из ZooAnimal: 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.
    Bear winnie( "
    любитель меда" );
    Raccoon meeko( "
    любитель всякой еды" );

    С++ для начинающих
    1   ...   85   86   87   88   89   90   91   92   93


    написать администратору сайта