Ereditarietà e classi derivate in C++

Ereditarietà e classi derivate in C++

L'ereditarietà è uno dei pilastri della programmazione orientata agli oggetti. Permette di definire una nuova classe a partire da una classe esistente, ereditandone i membri dati e i metodi e aggiungendo o specializzando il comportamento. In questo modo si modella la relazione concettuale "è un tipo di": un cane è un animale, un dipendente è una persona, un quadrato è una figura. L'ereditarietà favorisce il riuso del codice ed esprime gerarchie di concetti che riflettono il dominio del problema.

Classe base e classe derivata

La classe da cui si eredita è detta classe base, mentre la nuova classe che eredita è detta classe derivata. La derivazione si esprime indicando, dopo il nome della classe derivata e i due punti, la modalità di accesso e il nome della classe base. La derivazione pubblica, la più comune, mantiene la relazione "è un tipo di" e conserva la visibilità originale dei membri ereditati.

#include <iostream>
#include <string>

class Animal {
protected:
    std::string name;

public:
    Animal(const std::string& animalName) : name(animalName) {}

    void breathe() const {
        std::cout << name << " respira" << std::endl;
    }
};

// Dog eredita pubblicamente da Animal
class Dog : public Animal {
public:
    Dog(const std::string& dogName) : Animal(dogName) {}

    void bark() const {
        std::cout << name << " abbaia" << std::endl;
    }
};

int main() {
    Dog rex("Rex");
    rex.breathe();   // Metodo ereditato da Animal
    rex.bark();      // Metodo proprio di Dog
    return 0;
}

L'oggetto rex dispone sia del metodo bark, definito in Dog, sia del metodo breathe, ereditato da Animal. Questo dimostra come la classe derivata estenda le capacità della classe base senza doverne riscrivere il codice.

Lo specificatore protected

Oltre a public e private esiste un terzo specificatore di accesso: protected. I membri protetti non sono accessibili dall'esterno della classe, come quelli privati, ma restano accessibili dalle classi derivate. Nell'esempio precedente il membro name è dichiarato protetto proprio per consentire alla classe Dog di utilizzarlo direttamente, pur rimanendo nascosto al codice esterno.

L'ordine di costruzione

Quando si crea un oggetto di una classe derivata, viene invocato prima il costruttore della classe base e solo successivamente quello della classe derivata. Per passare gli argomenti al costruttore della classe base si utilizza la lista di inizializzazione, indicando il nome della classe base seguito dagli argomenti. La distruzione avviene nell'ordine inverso: prima il distruttore della classe derivata, poi quello della classe base.

#include <iostream>

class Base {
public:
    Base() { std::cout << "Costruttore Base" << std::endl; }
    ~Base() { std::cout << "Distruttore Base" << std::endl; }
};

class Derived : public Base {
public:
    Derived() { std::cout << "Costruttore Derived" << std::endl; }
    ~Derived() { std::cout << "Distruttore Derived" << std::endl; }
};

Ridefinire i metodi ereditati

Una classe derivata può ridefinire un metodo ereditato dalla classe base fornendo una propria implementazione. Questo meccanismo, detto ridefinizione o shadowing, permette alla classe derivata di specializzare un comportamento generico. Se necessario, all'interno del metodo ridefinito è possibile richiamare esplicitamente la versione della classe base qualificandola con il nome della base e l'operatore di risoluzione di ambito.

class Shape {
public:
    void describe() const {
        std::cout << "Sono una figura generica" << std::endl;
    }
};

class Circle : public Shape {
public:
    // Ridefinisce il metodo della classe base
    void describe() const {
        Shape::describe();   // Richiama la versione della base
        std::cout << "In particolare sono un cerchio" << std::endl;
    }
};

Composizione contro ereditarietà

L'ereditarietà non è sempre la scelta migliore. Spesso una relazione "ha un" è meglio modellata dalla composizione, ovvero includendo un oggetto come membro dato anziché derivando da una classe. Un'automobile non è un motore, ma ha un motore: in questo caso la composizione è più appropriata. Una buona regola pratica è preferire la composizione all'ereditarietà quando non sussiste una chiara relazione "è un tipo di".

L'ereditarietà che abbiamo visto finora è statica: i metodi chiamati vengono decisi in fase di compilazione in base al tipo dichiarato. Per ottenere un comportamento che varia in base al tipo effettivo dell'oggetto a runtime occorre il polimorfismo, basato sulle funzioni virtuali, argomento del prossimo articolo.