Menu

Ereditarietà in C++: classi base e classi derivate

Scopri come l'ereditarietà in C++ permette a una classe derivata di riusare ed estendere una classe base: la sintassi, ereditarietà public vs private, l'ordine di costruttori e distruttori e le trappole come l'object slicing.

Questa pagina include editor eseguibili: modifica, esegui e vedi subito l'output.

Riusare una classe estendendola

Hai già costruito classi con i costruttori e le hai ripulite con i distruttori. L'ereditarietà è il passo successivo: invece di copiare i membri di una classe in un'altra, dichiari che una nuova classe è una versione specializzata di una esistente e le lasci ereditare tutto automaticamente.

La classe esistente è la classe base (o genitore); quella nuova è la classe derivata (o figlia). Una classe derivata parte con tutti i dati e il comportamento della base, poi aggiunge o modifica ciò che la rende diversa. È lo strumento principale di C++ per la relazione "is-a": un Dog è un Animal, un SavingsAccount è un BankAccount.

Sintassi di base: class Derived : public Base

Erediti facendo seguire al nome della classe derivata i due punti, uno specificatore di accesso e il nome della classe base. La forma più comune è l'ereditarietà public.

Dog non dichiara mai name né eat(), eppure entrambi funzionano su un oggetto Dog perché sono stati ereditati da Animal. La classe derivata è libera di aggiungere membri come bark() di cui la base non sa nulla.

protected: membri solo per le classi figlie

Un membro private della base non è accessibile dentro la classe derivata: l'ereditarietà non rompe l'incapsulamento. Quando vuoi che i dettagli interni della base restino nascosti al mondo esterno ma disponibili alle sottoclassi, usa lo specificatore di accesso protected.

Pensa ai tre livelli come ad anelli concentrici: private significa "solo questa classe", protected "questa classe e le sue discendenti", e public "tutti". Consulta access-specifiers per il quadro completo.

Ordine di costruttori e distruttori

Un oggetto derivato contiene un sotto-oggetto base, e quella parte base deve esistere prima che venga costruita la parte derivata. Quindi la costruzione parte dalla base, la distruzione parte dalla derivata (l'ordine esattamente inverso). Se la base richiede argomenti nel costruttore, li passi tramite la lista di inizializzazione dei membri.

L'output rende concreto l'ordine:

Animal ctor: Rex   // la base viene costruita per prima
Dog ctor           // poi la parte derivata
Dog dtor           // distrutti in ordine inverso...
Animal dtor: Rex   // ...la base per ultima

Se dimentichi : Animal(n) e la base non ha un costruttore di default, il codice non compila: C++ non ha idea di come costruire la parte base. Una classe base da cui intendi ereditare dovrebbe quasi sempre dichiarare un distruttore (e, come mostra la prossima pagina, spesso uno virtual).

Overriding: ridefinire un metodo della base

Una classe derivata può sostituire un metodo ereditato dichiarandone uno con la stessa firma. Puoi comunque raggiungere l'originale tramite Base::method().

Questo è semplice name hiding, non polimorfismo: quale describe() viene eseguito lo decide il compilatore in base al tipo statico della variabile. È un limite importante: se chiami tramite un Shape& o uno Shape* che in realtà punta a un Circle, ottieni comunque Shape::describe(). Per risolverlo serve virtual, l'argomento della prossima pagina.

Attenzione all'object slicing

Poiché un riferimento o un puntatore alla base può riferirsi a un oggetto derivato, viene la tentazione di copiare un oggetto derivato in una variabile di tipo base. Non farlo: la parte derivata viene tagliata via.

a è un vero Animal, non un Dog con l'etichetta della base, quindi su di esso breed semplicemente non esiste. Per lavorare con oggetti derivati in modo polimorfico devi usare un riferimento (Animal&) o un puntatore (Animal*) alla base, mai un valore di tipo base. Lo slicing è silenzioso: compila senza problemi e scarta dati senza avvisare, il che lo rende uno dei bug di ereditarietà più facili da portare in produzione.

Errori comuni da evitare

  • Aspettarsi che i membri private della base siano raggiungibili nella figlia. Non lo sono. Usa protected per i dati di cui la classe derivata ha legittimamente bisogno, e tieni private lo stato davvero interno.
  • Dimenticare di inoltrare gli argomenti al costruttore della base. Se la base non ha un costruttore di default, devi chiamarlo esplicitamente nella lista di inizializzazione del costruttore derivato (: Base(args)).
  • Tagliare un oggetto derivato copiandolo in un valore base. Copiare un Dog in un Animal scarta tutto ciò che è specifico di Dog. Passa e memorizza invece riferimenti o puntatori alla base.
  • Abusare dell'ereditarietà per riusare codice. L'ereditarietà modella "is-a". Se la relazione è in realtà "has-a" (un Car ha un Engine), preferisci la composizione, cioè un oggetto membro, all'ereditarietà.

Prossimo passo: funzioni virtuali

L'override che hai appena visto è stato risolto in fase di compilazione, quindi chiamare tramite un puntatore alla base ha ignorato la versione derivata. La prossima pagina, virtual-functions, introduce la parola chiave virtual e override: il meccanismo che fa decidere al tipo a runtime quale metodo eseguire, sblocca il vero polimorfismo e spiega perché le classi base hanno bisogno di distruttori virtuali.

Domande frequenti

Cos'è l'ereditarietà in C++?

L'ereditarietà ti permette di definire una nuova classe (la classe derivata) a partire da una esistente (la classe base). La classe derivata ottiene automaticamente i dati membro e le funzioni membro della base, e può aggiungerne di nuovi o sostituire il comportamento esistente. Modella una relazione "is-a" (un Dog è un Animal) ed è il modo principale con cui C++ riusa ed estende il codice nelle gerarchie di classi.

Qual è la differenza tra ereditarietà public e private in C++?

Con l'ereditarietà public (class Dog : public Animal), l'interfaccia pubblica della base resta pubblica nella classe derivata, quindi un Dog è un Animal e può essere usato ovunque serva un Animal. Con l'ereditarietà private, i membri ereditati diventano privati: la classe derivata riusa l'implementazione della base ma non può sostituirla. L'ereditarietà public è di gran lunga il caso più comune; usa quella private solo per il riuso "implementato in termini di".

In che ordine vengono eseguiti costruttori e distruttori con l'ereditarietà in C++?

I costruttori partono dalla base: la classe base viene costruita completamente prima che venga eseguito il corpo del costruttore derivato. I distruttori seguono l'ordine esattamente inverso: prima la derivata, poi la base. Così, mentre un oggetto derivato viene costruito o distrutto, ogni parte da cui dipende esiste già (o esiste ancora).

Illustrazione dei linguaggi di programmazione di Coddy

Impara a programmare con Coddy

INIZIA