Condividi tramite


Più classi base

Una classe può essere derivata da più classi di base. In un modello di ereditarietà multipla (in cui le classi sono derivate da più classi di base), le classi di base vengono specificate usando l'elemento grammaticale di elenco di base. Ad esempio, la dichiarazione di classe per CollectionOfBook, derivata da Collection e Book, può essere specificata come segue:

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

L'ordine in cui vengono specificate le classi di base non è significativo tranne in alcuni casi in cui vengono richiamati costruttori e distruttori. In questi casi, l'ordine in cui le classi base vengono specificate influisce su quanto segue:

  • Ordine in cui vengono chiamati i costruttori. Se il codice si basa sul presupposto che la parte Book di CollectionOfBook sia inizializzata prima della parte Collection, l'ordine di specifica è significativo. L'inizializzazione viene eseguita nell'ordine in cui le classi vengono specificate nell'elenco di base.

  • L'ordine con cui vengono richiamati i distruttori per eseguire la pulizia. Anche in questo caso, se una "parte" specifica della classe deve essere presente mentre l'altra parte viene eliminata definitivamente, l'ordine è significativo. I distruttori vengono chiamati nell'ordine inverso delle classi specificate nell'elenco di base.

    Nota

    L'ordine di specifica delle classi base può influire sul layout di memoria della classe. Non prendere decisioni relative alla programmazione in base all'ordine dei membri base nella memoria.

Quando si specifica l'elenco di base, non è possibile specificare più volte lo stesso nome di classe. Tuttavia, è possibile che una classe sia una base indiretta a una classe derivata più volte.

Classi di base virtuali

Poiché una classe può rappresentare più di una volta una classe base indiretta per una classe derivata, in C++ è possibile ottimizzare l'utilizzo di tali classi base. Le classi base virtuali consentono di risparmiare spazio e di evitare ambiguità in gerarchie di classi in cui viene utilizzata l'ereditarietà multipla.

Ogni oggetto non virtuale contiene una copia dei membri dati definiti nella classe base. Questa duplicazione determina uno spreco di spazio e richiede di specificare la copia dei membri della classe base desiderata ogni volta che vi si accede.

Quando una classe base viene specificata come base virtuale, può essere utilizzata più volte come base indiretta senza duplicazione dei membri dati. Una sola copia dei membri dati viene condivisa da tutte le classi base che la utilizzano come base virtuale.

Quando si dichiara una classe base virtuale, la virtual parola chiave viene visualizzata negli elenchi di base delle classi derivate.

Si consideri la gerarchia di classi nella figura seguente, che illustra una linea di pranzo simulata:

Diagram of a simulated lunch line.

La classe di base è Queue. La coda cashier e la coda pranzo ereditano entrambe dalla coda. Infine, La coda di cassa pranzo eredita sia dalla coda cassiere che dalla coda pranzo.

Grafico a linee pranzo simulato

Nella figura Queue è la classe base sia per CashierQueue che per LunchQueue. Tuttavia, quando entrambe le classi vengono combinate per formare LunchCashierQueue, si verifica il problema seguente: la nuova classe contiene due oggetti secondari di tipo Queue, uno da CashierQueue e l'altro da LunchQueue. La figura seguente illustra il layout concettuale della memoria (il layout effettivo della memoria potrebbe essere ottimizzato):

Diagram of a simulated lunch line object.

La figura mostra un oggetto Lunch Cashier Queue con due sottooggetti: Cassaier Queue e Lunch Queue. Sia la coda cashier che la coda pranzo contengono un sottooggetto Coda".

Oggetto linea pranzo simulato

Nell'oggetto LunchCashierQueue sono presenti due Queue sottooggetti. Nel codice seguente Queue viene dichiarato come una classe base virtuale:

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

La virtual parola chiave garantisce che sia inclusa una sola copia del sottooggetto Queue (vedere la figura seguente).

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

Il diagramma mostra un oggetto Lunch Cashier Queue, che contiene un sottooggetto Cashier Queue e un sottooggetto Lunch Queue. Sia Cashier Queue che Lunch Queue condividono lo stesso sottooggetto Queue.

Oggetto linea pranzo simulato con classi di base virtuali

Alla classe possono essere associati sia un componente virtuale che uno non virtuale di un tipo specifico. Ciò si verifica nelle condizioni illustrate nella figura seguente:

Diagram of virtual and non virtual components of a class.

Il diagramma mostra una classe base della coda. Una classe Cashier Queue e la classe Lunch Queue ereditano virtualmente da Queue. Una terza classe, Takeout Queue, eredita non virtualmente dalla coda. Lunch Cashier Queue eredita sia dalla coda cashier che dalla coda pranzo. Lunch Takeout Cashier Queue eredita sia dalla coda del cassiere pranzo che dalla coda takeout.

Componenti virtuali e non virtuali della stessa classe

Nella figura CashierQueue e LunchQueue usano Queue come classe base virtuale. Tuttavia, TakeoutQueue specifica Queue come classe base, non come classe base virtuale. Di conseguenza, LunchTakeoutCashierQueue dispone di due oggetti secondari di tipo Queue: uno dal percorso di ereditarietà che include LunchCashierQueue e uno dal percorso che include TakeoutQueue. Questa situazione viene illustrata nella figura seguente.

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

Viene visualizzato un oggetto Lunch Takeout Cashier Queue che contiene due sottooggetti: una coda takeout (che contiene un sottooggetto Queue) e una coda di cassa pranzo. Il sottooggetto Lunch Cashier Queue contiene un sottooggetto Cashier Queue e un sottooggetto Lunch Queue, entrambi che condividono un sottooggetto Queue.

Layout di oggetti con ereditarietà virtuale e non virtuale

Nota

L'ereditarietà virtuale offre vantaggi significativi in termini di dimensione se paragonata all'ereditarietà non virtuale. Può introdurre tuttavia un ulteriore sovraccarico di elaborazione.

Se una classe derivata esegue l'override di una funzione virtuale che eredita da una classe base virtuale e se un costruttore o un distruttore per la classe base derivata chiama tale funzione usando un puntatore alla classe base virtuale, il compilatore può introdurre altri campi "vtordisp" nascosti nelle classi con basi virtuali. L'opzione /vd0 del compilatore elimina l'aggiunta del membro di spostamento costruttore/distruttore vtordisp nascosto. L'opzione /vd1 del compilatore, predefinita, consente di abilitarle dove sono necessarie. Disattivare vtordisps solo se si è certi che tutti i costruttori e i distruttori di classe chiamano virtualmente le funzioni virtuali.

L'opzione /vd del compilatore influisce su un intero modulo di compilazione. Usare il vtordisp pragma per eliminare e quindi riabilitare vtordisp i campi in base alla classe:

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

Ambiguità di nomi

L'ereditarietà multipla introduce la possibilità che i nomi vengano ereditati attraverso più di un percorso. I nomi dei membri di classe lungo questi percorsi non sono necessariamente univoci. Questi conflitti di nomi sono denominati "ambiguità."

Qualsiasi espressione che fa riferimento a un membro di classe deve creare un riferimento non ambiguo. Nell'esempio che segue viene illustrato come si sviluppano le ambiguità:

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

Date le dichiarazioni di classe precedenti, il codice come il seguente è ambiguo perché non è chiaro se b fa riferimento a b in A o in B:

C *pc = new C;

pc->b();

Se consideri l'esempio precedente. Poiché il nome a è un membro sia della classe A che della classe B, il compilatore non può distinguere la a funzione da chiamare. L'accesso a un membro è ambiguo se può fare riferimento a più funzioni, oggetti, tipi o enumeratori.

Il compilatore rileva le ambiguità eseguendo dei test nell'ordine seguente:

  1. Se l'accesso al nome è ambiguo, secondo quanto appena descritto, viene generato un messaggio di errore.

  2. Se le funzioni di overload non sono ambigue, vengono risolte.

  3. Se l'accesso al nome viola le autorizzazioni di accesso ai membri, viene generato un messaggio di errore. Per altre informazioni, vedere Member-Controllo di accesso.)

Quando un'espressione produce ambiguità attraverso l'ereditarietà, è possibile risolverla manualmente qualificando il nome in questione con il relativo nome di classe. Per fare in modo che l'esempio precedente venga compilato correttamente e senza ambiguità, utilizzare codice analogo al seguente:

C *pc = new C;

pc->B::a();

Nota

Quando C viene dichiarata, è possibile che la stessa generi errori, quando si fa riferimento a B nell'ambito di C. In ogni caso, fino a quando un riferimento non qualificato a B non venga effettivamente utilizzato nell'ambito di C, non viene generato alcun errore.

Dominanza

è possibile che più nomi (funzione, oggetto o enumeratore) vengano raggiunti tramite un grafico di ereditarietà. Questi casi sono considerati ambigui con le classi base non virtuali. Sono ambigui anche con le classi di base virtuali, a meno che uno dei nomi "domina" gli altri.

Un nome domina un altro nome se è definito in entrambe le classi e una classe è derivata dall'altra. Il nome dominante è il nome nella classe derivata; questo nome viene utilizzato quando sarebbe altrimenti sorta una certa ambiguità, come mostrato nell'esempio seguente:

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

Conversioni ambigue

Le conversioni esplicite e implicite da puntatori o riferimenti ai tipi di classe possono causare ambiguità. Nella figura seguente, conversione ambigua dei puntatori alle classi base, viene illustrato quanto segue:

  • Dichiarazione di un oggetto di tipo D.

  • Effetto dell'applicazione dell'operatore address-of (&) a tale oggetto. L'operatore address-of fornisce sempre l'indirizzo di base dell'oggetto.

  • Effetto della conversione esplicita del puntatore ottenuto usando l'operatore address-of in un tipo A della classe base. La coercing dell'indirizzo dell'oggetto da digitare A* non fornisce sempre al compilatore informazioni sufficienti su quale sottooggetto di tipo A selezionare. In questo caso, esistono due sottooggetti.

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

Il diagramma mostra innanzitutto una gerarchia di ereditarietà: A è la classe di base. B e C ereditano da A. D eredita da B e C. Viene quindi visualizzato il layout della memoria per l'oggetto D. Esistono tre sottooggetti in D: B (che include un sottooggetto A) e C (che include un sottooggetto A). Il codice & d punta all'oggetto A nel sottooggetto B. Il codice ( * A ) & d punta sia al sottooggetto B che al sottooggetto C.

Conversione ambigua di puntatori in classi di base

La conversione al tipo A* (puntatore a A) è ambigua perché non esiste alcun modo per distinguere il sottooggetto di tipo A corretto. È possibile evitare l'ambiguità specificando in modo esplicito quale sottooggetto si intende usare, come indicato di seguito:

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

Ambiguità e classi base virtuali

Se si usano le classi base virtuali, è possibile passare a funzioni, oggetti, tipi ed enumeratori mediante i percorsi di ereditarietà multipla. Poiché è presente una sola istanza della classe di base, non esiste alcuna ambiguità durante l'accesso a questi nomi.

La figura seguente mostra come vengono composti gli oggetti mediante l'ereditarietà virtuale e non virtuale.

Diagram showing virtual derivation and nonvirtual derivation.

Il diagramma mostra innanzitutto una gerarchia di ereditarietà: A è la classe di base. B e C ereditano virtualmente da A. D eredita virtualmente da B e C. Viene quindi visualizzato il layout di D. D contiene oggetti secondari B e C, che condividono il sottooggetto A. Il layout viene quindi visualizzato come se la stessa gerarchia fosse stata derivata usando l'ereditarietà non virtuale. In tal caso, D contiene gli oggetti secondari B e C. Sia B che C contengono la propria copia del sottooggetto A.

Derivazione virtuale e non virtuale

Nella figura l'accesso a qualsiasi membro della classe A tramite le classi base non virtuali causa ambiguità; il compilatore non dispone di informazioni che spieghino se utilizzare l'oggetto secondario associato a B o l'oggetto secondario associato a C. Tuttavia, quando A viene specificato come classe base virtuale, non c'è dubbio quale sottooggetto è accessibile.

Vedi anche

Ereditarietà