Wiele klas podstawowych

Klasa może pochodzić z więcej niż jednej klasy bazowej. W modelu wielokrotnego dziedziczenia (gdzie klasy pochodzą z więcej niż jednej klasy bazowej), klasy bazowe są określane przy użyciu elementu gramatyki listy bazowej. Na przykład, można określić deklarację klasy dla CollectionOfBook, pochodzącej z Collection i Book:

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

Kolejność, w której określono klasy bazowe, nie jest znacząca, z wyjątkiem niektórych przypadków, w których wywoływane są konstruktory i destruktory. W tych przypadkach, kolejność, w której określane są klasy bazowe ma wpływ na:

  • Kolejność wywoływanych konstruktorów. Jeśli kod opiera się na tym, aby część Book kolekcji CollectionOfBook została zainicjowana przed częścią Collection, kolejność specyfikacji ma znaczenie. Inicjowanie odbywa się w kolejności, w których klasy są określone na liście bazowej.

  • Kolejność, w której wywoływane są destruktory, aby dokonać czyszczenia. Ponownie, jeśli określona „część” klasy musi być obecna, gdy niszczona jest inna część, kolejność ma znaczenie. Destruktory są wywoływane w odwrotnej kolejności klas określonych na liście bazowej.

    Uwaga

    Kolejność specyfikacji klas bazowych może wpływać na układ pamięci klasy. Nie należy podejmować żadnych decyzji programistycznych na podstawie kolejności elementów podstawowych w pamięci.

Podczas określania listy podstawowej nie można określić tej samej nazwy klasy więcej niż raz. Istnieje jednak możliwość, aby klasa była pośrednią bazą dla klasy pochodnej więcej niż raz.

Wirtualne klasy bazowe

Ponieważ klasa może być pośrednią klasą bazową do klasy pochodnej więcej niż raz, w języku C++ zapewniono możliwość optymalizacji sposobu działania klas bazowych. Wirtualne klasy bazowe oferują sposób, aby zaoszczędzić miejsce i uniknąć niejasności w hierarchii klas, które używają wielokrotnego dziedziczenia.

Każdy obiekt niewirtualny zawiera kopię składowych danych zdefiniowanych w klasie podstawowej. Ta duplikacja powoduje utratę miejsca i wymaga od użytkownika wybrania kopii składowych klasy podstawowej przy każdym uzyskiwaniu dostępu do nich.

Gdy klasa podstawowa jest określona jako baza wirtualna, może ona działać jako baza pośrednia więcej niż raz, bez duplikacji jej składowych danych. Pojedyncza kopia składowych danych jest współużytkowana przez wszystkie klasy podstawowe, które używają jej jako bazy wirtualnej.

Podczas deklarowania wirtualnej klasy virtual bazowej słowo kluczowe pojawia się na listach podstawowych klas pochodnych.

Rozważ hierarchię klas na poniższej ilustracji, która ilustruje symulowaną linię lunchu:

Diagram of a simulated lunch line.

Klasa bazowa to Queue. Kolejka kasjera i kolejka lunchu dziedziczą z kolejki. Wreszcie, Lunch Cashier Queue dziedziczy zarówno z kolejki kasjerów, jak i kolejki lunchu.

Symulowany wykres liniowy lunchu

Na rysunku Queue jest klasą bazową dla CashierQueue i LunchQueue. Jednakże, gdy obie klasy są połączone, tworząc LunchCashierQueue, pojawia się następujący problem: nowa klasa zawiera dwa podobiekty typu Queue, jeden CashierQueue, a drugi LunchQueue. Na poniższej ilustracji przedstawiono koncepcyjny układ pamięci (rzeczywisty układ pamięci może zostać zoptymalizowany):

Diagram of a simulated lunch line object.

Na rysunku przedstawiono obiekt Kolejka kasjera obiadowego z dwoma podobiektami: Kolejka kasjera i kolejka lunchu. Zarówno kolejka kasjera, jak i kolejka lunchu zawierają podobiekt kolejki kolejki.

Symulowany obiekt linii lunchu

W obiekcie znajdują się dwa Queue podobiekty LunchCashierQueue . W poniższym kodzie Queue zadeklarowano jako wirtualną klasę bazową:

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

Słowo virtual kluczowe gwarantuje, że dołączono tylko jedną kopię podobiektu Queue (zobacz poniższą ilustrację).

Diagram of a simulated lunch line object, with virtual base classes depicted.

Na diagramie przedstawiono obiekt Kolejka kasjera obiadowego, który zawiera podobiekt Kolejka kasjera i podobiekt kolejki lunchu. Zarówno kolejka kasjera, jak i kolejka lunchu współdzielą ten sam podobiekt Kolejka kolejki.

Symulowany obiekt linii lunchu z wirtualnymi klasami bazowymi

Klasa może mieć zarówno wirtualny jak i niewirtualny składnik danego typu. Dzieje się tak w warunkach przedstawionych na poniższej ilustracji:

Diagram of virtual and non virtual components of a class.

Diagram przedstawia klasę bazową kolejki. Klasa Kolejka kasjera i kolejka lunchu dziedziczą praktycznie z kolejki. Trzecia klasa, Takeout Queue, dziedziczy nie wirtualnie z kolejki. Kolejka kasjera obiadowego dziedziczy zarówno z kolejki kasjerowej, jak i kolejki lunchu. Lunch Takeout Cashier Queue dziedziczy zarówno z kolejki kasjera lunchu, jak i kolejki na wynos.

Składniki wirtualne i niewirtualne tej samej klasy

Na rysunku CashierQueue i LunchQueue używają Queue jako wirtualnej klasy bazowej. Jednak TakeoutQueue określa Queue jako klasę bazową, a nie wirtualną klasę bazową. W związku z tym LunchTakeoutCashierQueue posiada dwa podobiekty typu Queue: jeden ze ścieżki dziedziczenia, która zawiera LunchCashierQueue a drugi ze ścieżki, która zawiera TakeoutQueue. To zostało zilustrowane na poniższym rysunku.

Diagram of the object layout for virtual and non virtual inheritance.

Zostanie wyświetlony obiekt Kolejka wyjęcia lunchu, który zawiera dwa podobiekty: kolejkę wyjętą (zawierającą podobiekt kolejki kolejki) i kolejkę kasjera obiadowego. Podobiekt Kolejka kasjera lunchu zawiera podobiekt Kolejka kasjera i podobiekt kolejki lunchu, który współużytkuje obiekt podrzędny Kolejka kolejki.

Układ obiektów z dziedziczeniem wirtualnym i niewirtualnym

Uwaga

Dziedziczenie wirtualne zapewnia znaczące korzyści dotyczące rozmiaru w porównaniu do dziedziczenia niewirtualnego. Jednak może to wprowadzić dodatkowe obciążenie dotyczące przetwarzania.

Jeśli klasa pochodna zastępuje funkcję wirtualną dziedziczą z wirtualnej klasy bazowej, a jeśli konstruktor lub destruktor pochodnej klasy bazowej wywołuje tę funkcję przy użyciu wskaźnika do wirtualnej klasy bazowej, kompilator może wprowadzić inne ukryte pola "vtordisp" do klas wirtualnych baz. Opcja /vd0 kompilatora pomija dodanie ukrytego elementu składowego przemieszczenia konstruktora/destruktora vtordisp. Opcja kompilatora /vd1 , domyślna, włącza je tam, gdzie są niezbędne. Wyłącz funkcję vtordisps tylko wtedy, gdy masz pewność, że wszystkie konstruktory klas i destruktory praktycznie wywołają funkcje wirtualne.

Opcja kompilatora /vd ma wpływ na cały moduł kompilacji. vtordisp Użyj pragma, aby pominąć, a następnie ponownie włączyć vtordisp pola na podstawie klasy według klasy:

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

Niejednoznaczności nazw

Wielokrotne dziedziczenie wprowadza dla nazw możliwość dziedziczenia wzdłuż więcej niż jednej ścieżki. Nazwy składowych klasy wzdłuż tych ścieżek nie muszą być unikatowe. Te konflikty nazw są nazywane „niejasnościami”.

Dowolne wyrażenie, które odwołuje się do składowej klasy, musi być jednoznacznym odniesieniem. W poniższym przykładzie pokazano, jak rozwijają się niejasności:

// 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 {};

Biorąc pod uwagę poprzednie deklaracje klas, kod, taki jak poniżej, jest niejednoznaczny, ponieważ nie jest jasne, czy b odnosi się do b elementu in A , czy w B:

C *pc = new C;

pc->b();

Należy rozważyć poprzedni przykład. Ponieważ nazwa a jest elementem członkowskim zarówno klasy, jak A i klasy B, kompilator nie może rozpoznać, który a wyznacza funkcję do wywołania. Dostęp do elementu członkowskiego jest niejednoznaczny, jeżeli może odnosić się do więcej niż jednej funkcji, obiektu, typu lub modułu wyliczającego.

Kompilator wykrywa niejasności wykonując testy w następującej kolejności:

  1. Jeśli dostęp do nazwy jest niejednoznaczny (jak opisano powyżej), generowany jest komunikat o błędzie.

  2. Jeśli przeciążone funkcje są jednoznaczne, zostaną rozwiązane.

  3. Jeśli dostęp do nazwy narusza uprawnienia dostępu do elementu członkowskiego, generowany jest komunikat o błędzie. (Aby uzyskać więcej informacji, zobacz Kontrola dostępu do składowych).

Gdy wyrażenie powoduje niejednoznaczność poprzez dziedziczenie, można ją rozwiązać ręcznie kwalifikując nazwę w pytaniu o nazwę klasy. Aby w poprzednim przykładzie skompilować poprawnie, bez niejasności, należy użyć kodu takiego jak:

C *pc = new C;

pc->B::a();

Uwaga

Gdy zadeklarowane jest C, może ono powodować błędy, podczas gdy ma miejsce odwołanie do B w zakresie C. Błąd nie jest zgłaszany, do chwili faktycznego dokonania niekwalifikowanego odwołania do B w zakresie C.

Dominacja

Istnieje możliwość osiągnięcia więcej niż jednej nazwy (funkcji, obiektu lub modułu wyliczającego) za pośrednictwem grafu dziedziczenia. Takie przypadki są uznawane za niejednoznaczne w przypadku niewirtualnych klas bazowych. Są one również niejednoznaczne w przypadku wirtualnych klas bazowych, chyba że jedna z nazw "dominuje" innych.

Nazwa dominuje w innej nazwie, jeśli jest zdefiniowana w obu klasach, a jedna klasa pochodzi od drugiej. Nazwa dominująca to nazwa w klasie pochodnej; Ta nazwa jest używana, gdy wystąpiłaby niejednoznaczność, jak pokazano w poniższym przykładzie:

// 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.
};

Niejednoznaczne konwersje

Jawne i niejawne konwersje ze wskaźników lub odwołań do typów klas mogą powodować niejednoznaczności. Na następnej ilustracji, niejednoznacznej konwersji wskaźników na klasy bazowe, przedstawiono następujące elementy:

  • Deklaracja obiektu typu D.

  • Efekt zastosowania operatora address-of (&) do tego obiektu. Operator address-of zawsze dostarcza podstawowy adres obiektu.

  • Efekt jawnego przekonwertowania wskaźnika uzyskanego przy użyciu operatora address-of na typ Aklasy bazowej . Coercing adres obiektu do typu A* nie zawsze dostarcza kompilatorowi wystarczające informacje o tym, który podobiekt typu A wybrać; w tym przypadku istnieją dwa podobiekty.

Diagram showing how the conversion of pointers to base classes can be ambiguous.

Diagram najpierw przedstawia hierarchię dziedziczenia: A jest klasą bazową. B i C dziedziczą z A. D dziedziczą z B i C. Następnie układ pamięci jest wyświetlany dla obiektu D. Istnieją trzy podobiekty w D: B (w tym podobiekt A) i C (który zawiera podobiekt A). Kod i d wskazuje obiekt A w podobiekcie B. Kod ( * A ) i d wskazuje zarówno podobiekt B, jak i podobiekt C.

Niejednoznaczna konwersja wskaźników na klasy bazowe

Konwersja na typ A* (wskaźnik do A) jest niejednoznaczna, ponieważ nie ma możliwości rozpoznania, który podobiekt typu A jest poprawny. Można uniknąć niejednoznaczności, jawnie określając podobiekt, który ma być używany, w następujący sposób:

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

Niejednoznaczności i wirtualne klasy bazowe

Jeśli są używane wirtualne klasy bazowe, funkcje, obiekty, typy i moduły wyliczane można uzyskać za pośrednictwem ścieżek wielokrotnego dziedziczenia. Ponieważ istnieje tylko jedno wystąpienie klasy bazowej, nie ma wątpliwości podczas uzyskiwania dostępu do tych nazw.

Na poniższej ilustracji pokazano, jak obiekty składają się przy użyciu dziedziczenia wirtualnego i niewirtualnego.

Diagram showing virtual derivation and nonvirtual derivation.

Diagram najpierw przedstawia hierarchię dziedziczenia: A jest klasą bazową. B i C praktycznie dziedziczą z A. D praktycznie dziedziczą z B i C. Następnie wyświetlany jest układ D. D zawiera podobiekty B i C, które współużytkuje podobiekt A. Następnie układ jest wyświetlany tak, jakby ta sama hierarchia została pochodna przy użyciu dziedziczenia niewirtualnego. W takim przypadku D zawiera podobiekty B i C. Zarówno B, jak i C zawierają własną kopię podobiektu A.

Wyprowadzanie wirtualne i niewirtualne

Na rysunku uzyskiwanie dostępu do dowolnego elementu członkowskiego klasy A za pomocą klas niewirtualnych powoduje niejednoznaczność. Kompilator nie zawiera żadnych informacji wyjaśniających, czy używać podobiektu skojarzonego z obiektem B , czy podobiektu skojarzonego z elementem C. Jednak jeśli A jest określona jako wirtualna klasa bazowa, nie ma wątpliwości, do którego podobiektu jest uzyskiwany dostęp.

Zobacz też

Dziedziczenie