Поделиться через


Несколько базовых классов

Класс может быть производным от нескольких базовых классов. В модели с несколькими наследованиеми (где классы, производные от нескольких базовых классов), базовые классы задаются с помощью элемента грамматики базового списка . Например, объявление класса для CollectionOfBook, производного от Collection и Book, можно указать следующим образом.

// deriv_MultipleBaseClasses.cpp
// compile with: /LD
class Collection {
};
class Book {};
class CollectionOfBook : public Book, public Collection {
    // New members
};

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

  • Порядок вызова конструкторов. Если код основан на том, что инициализация части BookCollectionOfBook должна выполняться перед частью Collection, порядок указания важен. Инициализация выполняется в порядке, который классы указываются в базовом списке.

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

    Примечание.

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

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

Виртуальные базовые классы

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

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

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

При объявлении виртуального базового класса virtual ключевое слово отображается в базовых списках производных классов.

Рассмотрим иерархию классов на следующем рисунке, которая иллюстрирует имитированную строку обеда:

Схема имитированной линии обеда.

Базовый класс — Queue. Очередь кассира и очередь обеда наследуются от очереди. Наконец, очередь обеда кассира наследует как от очереди кассира, так и от очереди обеда.

Имитированный график на обеде

Как видно на рисунке, класс Queue является базовым для двух других классов: CashierQueue и LunchQueue. Однако когда эти два класса объединяются и образуют класс LunchCashierQueue, возникает следующая проблема: новый класс содержит два подчиненных объекта типа Queue — один из CashierQueue, а другой из LunchQueue. На следующем рисунке показан макет концептуальной памяти (фактический макет памяти может быть оптимизирован):

Схема имитированного объекта линии обеда.

На рисунке показан объект очереди "Обед" с двумя подобъектами в нем: очередь кассира и очередь обеда. Как очередь кассира, так и очередь обеда содержат вложенный объект очереди".

Имитированный объект обеда

В объекте есть два Queue вложенных LunchCashierQueue объекта. В следующем коде содержится объявление Queue как виртуального базового класса:

// deriv_VirtualBaseClasses.cpp
// compile with: /LD
class Queue {};
class CashierQueue : virtual public Queue {};
class LunchQueue : virtual public Queue {};
class LunchCashierQueue : public LunchQueue, public CashierQueue {};

Ключевое virtual слово гарантирует, что включена только одна копия подобъекта Queue (см. следующий рисунок).

Схема имитированного объекта линии обеда с изображенными виртуальными базовыми классами.

На схеме показан объект "Очередь обеда", содержащий подобъект очереди "Кассиер" и подобъект очереди обеда. В очереди кассира и очереди обеда используются одни и те же подобъекты очереди.

Имитация объекта обеда с виртуальными базовыми классами

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

Схема виртуальных и не виртуальных компонентов класса.

На схеме показан базовый класс очереди. Класс "Кассир очереди" и класс "Очередь обеда" наследуются практически от очереди. Третий класс, takeout Queue, наследует не практически из очереди. Очередь обеда кассира наследует от очереди кассира и очереди обеда. Очередь обеда Takeout Cashier наследует как от очереди обеда кассира, так и очереди выноса.

Виртуальные и невиртуальные компоненты одного класса

На этом рисунке показано, что классы CashierQueue и LunchQueue используют Queue как виртуальный базовый класс. Однако TakeoutQueue определяет Queue в качестве базового класса, а не виртуального базового класса. Поэтому в LunchTakeoutCashierQueue имеется два подчиненных объекта типа Queue: один из пути наследования, включающего LunchCashierQueue, а второй из пути, включающего TakeoutQueue. Это показано на следующем рисунке.

Схема макета объекта для виртуального и не виртуального наследования.

Отображается объект очереди Takeout Cashier, содержащий два подобъекта: очередь выхода (которая содержит подобъект очереди очереди) и очередь обеда кассира. Подобъект очереди "Обед кассира" содержит подобъект очереди "Кассиер" и подобъект очереди обеда, оба из которых совместно используют вложенный объект очереди очереди.

Макет объекта с виртуальным и невиртуальным наследованием

Примечание.

Наследование от виртуальных базовых классов позволяет существенно сократить объем данных по сравнению с наследованием от невиртуальных классов. Однако оно может породить дополнительные затраты на обработку.

Если производный класс переопределяет виртуальную функцию, наследуемую от виртуального базового класса, и если конструктор или деструктор для производного базового класса вызывает функцию с помощью указателя на виртуальный базовый класс, компилятор может ввести другие скрытые поля vtordisp в классы с виртуальными базами. Параметр /vd0 компилятора подавляет добавление скрытого элемента конструктора или деструктора vtordisp. Параметр /vd1 компилятора по умолчанию включает их, где они необходимы. Отключите vtordisps, только если вы уверены, что все конструкторы классов и деструкторы вызывают виртуальные функции практически.

Параметр /vd компилятора влияет на весь модуль компиляции. vtordisp Используйте pragma для подавления и последующего повторного создания vtordisp полей на основе класса:

#pragma vtordisp( off )
class GetReal : virtual public { ... };
\#pragma vtordisp( on )

Неоднозначность имен

Множественное наследование предоставляет возможность наследования имен по нескольким путям. Имена членов класса по этим путям не обязательно уникальны. Эти конфликты имен называются неоднозначностями.

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

// deriv_NameAmbiguities.cpp
// compile with: /LD
// Declare two base classes, A and B.
class A {
public:
    unsigned a;
    unsigned b();
};

class B {
public:
    unsigned a();  // class A also has a member "a"
    int b();       //  and a member "b".
    char c;
};

// Define class C as derived from A and B.
class C : public A, public B {};

Учитывая предыдущие объявления класса, код, например следующий, неоднозначный, поскольку неясно, относится ли b к объекту b in A или in B:

C *pc = new C;

pc->b();

Рассмотрим предыдущий пример. Так как имя a является членом как класса, так и классаBA, компилятор не может определить, что a обозначает функцию для вызова. Доступ к члену неоднозначен, если он может ссылаться на несколько функций, объектов, типов или перечислителей.

Компилятор определяет неоднозначности, выполняя тесты в указанном порядке.

  1. Если доступ к имени неоднозначен (как описано выше), создается сообщение об ошибке.

  2. Если перегруженные функции являются однозначной, они разрешаются.

  3. Если доступ к имени нарушает разрешение доступа к членам, создается сообщение об ошибке. (Дополнительные сведения см. в разделе Член-контроль доступа.)

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

C *pc = new C;

pc->B::a();

Примечание.

Если объявлен C, могут возникнуть ошибки, если сослаться на B в области C. Однако ошибка не выдается, если не внести неквалифицированную ссылку на B в области C.

Доминирование

Можно получить несколько имен (функция, объект или перечислитель) с помощью графа наследования. С невиртуальными базовыми классами такие случаи неоднозначны. Они также неоднозначны с виртуальными базовыми классами, если только одно из имен "доминирует" других.

Имя доминирует другое имя, если оно определено в обоих классах, и один класс является производным от другого. Доминирующее имя — это имя в производном классе; оно используется тогда, когда в противном случае возникла бы неоднозначность, как показано в следующем примере.

// deriv_Dominance.cpp
// compile with: /LD
class A {
public:
    int a;
};

class B : public virtual A {
public:
    int a();
};

class C : public virtual A {};

class D : public B, public C {
public:
    D() { a(); } // Not ambiguous. B::a() dominates A::a.
};

Неоднозначные преобразования

Явные и неявные преобразования указателей и ссылок в типы класса могут приводить к неоднозначности. На следующем рисунке "Неоднозначное преобразование указателей в базовые классы" показано следующее:

  • Объявление объекта типа D.

  • Эффект применения оператора адреса (&) к объекту. Оператор address-of всегда предоставляет базовый адрес объекта.

  • Результат явного преобразования указателя, полученного с помощью оператора взятия адреса, в тип базового класса A. Принудение адреса объекта типа A* не всегда предоставляет компилятору достаточно сведений о том, какой подобъект типа A выбрать; в данном случае существует два подобъекта.

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

На схеме сначала показана иерархия наследования: это базовый класс. B и C наследуют от A. D наследует от B и C. Затем макет памяти отображается для объекта D. Существует три подобъекта в D: B (который включает в себя подобъект A) и C (который включает в себя вложенный объект A). Код и d указывает на объект A в подобъекте B. Код (* A) и d указывает на подобъект B и подобъект C.

Неоднозначное преобразование указателей на базовые классы

Преобразование в тип A* (указатель Aна) является неоднозначным, так как нет способа определить, какой подобъект типа A является правильным. Вы можете избежать неоднозначности, явно указав, какой подображаемый объект вы хотите использовать, как показано ниже.

(A *)(B *)&d       // Use B subobject.
(A *)(C *)&d       // Use C subobject.

Неоднозначности и виртуальные базовые классы

Если используются виртуальные базовые классы, доступ к функциям, объектам, типам и перечислениям можно получить по путям множественного наследования. Так как существует только один экземпляр базового класса, при доступе к этим именам нет неоднозначности.

На следующем рисунке показано составление объектов с использованием виртуального и невиртуального наследования.

Схема, на которой показаны виртуальные производные и невиртуальные производные.

На схеме сначала показана иерархия наследования: это базовый класс. B и C практически наследуются от A. D практически наследует от B и C. Затем отображается макет D. D содержит вложенные объекты B и C, которые совместно используют вложенный объект A. Затем макет показан, как если бы та же иерархия была производна с помощью невиртуального наследования. В этом случае D содержит вложенные объекты B и C. Оба объекта B и C содержат собственную копию подобъекта A.

Виртуальная и невиртуальная производная

На этом рисунке доступ к любому члену класса A через невиртуальная базовые классы вызывает неоднозначность; у компилятора нет сведений, поясняющих, нужно ли использовать вложенный объект, связанный с B, или вложенный объект, связанный с C. Однако при A указании в качестве виртуального базового класса нет никаких вопросов, к которым обращается подобъект.

См. также

Наследование