C++ Ereditarietà

Da Andreabont's Wiki.

Il C++ è un linguaggio che supporta la programmazione con paradigma ad oggetti. Tra oggetti è possibile definire il concetto di eredità. In C++ non esistono le interfacce come concetto a s'è stante, in quanto il linguaggio supporta l'ereditarietà multipla ed è possibile definire una classe astratta con tutti i metodi virtuali puri.

Modificatori di accesso

Quando si definisce l'ereditarietà di una classe è possibile specificare come questa classe accederà ai metodi della classe padre, in caso di ereditarietà multipla è necessario definire un modificatore di accesso per ogni classe padre.

Modificatore Descrizione
public Il contenuto della classe padre mantiene i suoi modificatori anche nella classe figlio.
protected Tutto ciò che è definito public nella classe padre diventa protected nella classe figlio.
private Tutto ciò che è public o protected nella classe padre diventa private nella classe figlio.

Comportamento

Il C++ definisce le regole di comportamento nell'ereditarietà nel suo standard, al fine di garantire una corretta esecuzione del codice.

Ordine di chiamata dei costruttori e dei distruttori

Nel caso si stia istanziando una classe che eredita da un'altra, è necessario gestire il costruttore ed il distruttore della classe padre:

#include <iostream>

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

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

int main () {
    B b;
}

Per garantire la consistenza dei dati, il C++ prima chiama il costruttore della classe padre, e poi il costruttore della classe figlio, mentre per il distruttore fa l'inverso, e distrugge prima la classe figlio e poi la classe padre. L'output atteso è:

Costruttore di A
Costruttore di B
Distruttore di B
Distruttore di A

Esempi

Ereditarietà semplice

class A {
    // Codice padre
};

class B : public A {
    // Codice figlio
};

Ereditarietà multipla

class A {
    // Codice padre 1
};

class B {
    // Codice padre 2
};

class C : public A, public B {
    // Codice figlio
};

Sovrascrivere i metodi del padre

Il C++ è possibile eseguire l'overriding dei metodi del padre, specificando una versione di questo metodo specifica per il figlio. Per farlo il padre deve definire quel metodo come virtual, in modo che il compilatore possa trattare questo metodo in modo dinamico a run-time.

class A {
    virtual void miometodo() {
        // Codice padre...
    }
};

class B : public A {
    void miometodo() override {
        // Codice figlio...
    }
}

Classe astratta

Una classe astratta contenente solo metodi virtuali puri può essere usata come interfaccia. Per definire un metodo come virtuale puro basta definirne il prototipo preceduto dalla parola chiave virtual (che serve a specificare la possibilità del figlio di sovrascrivere il metodo del padre sfruttando le vtable) e seguito dalla sintassi "= 0", che ne indica la volontaria mancanza di impelmentazione. Da notare che è comunque possibile sovrascrivere un metodo non dichiarato come virtual, ma può generare dei comportamenti indesiderati (i metodi della classe padre chiamerebbero sempre il metodo base, e non quello sovrascritto dal figlio)

class A {
    virtual void miometodo() = 0;
};

class B : public A {
    // Se voglio istanziare questa classe sono obbligato ad implementare miometodo().
}

Questa tecnica può essere usata anche per ciclare su classi diverse, purché queste implementino la stessa interfaccia. Con l'unico vincolo che saremo costretti ad usare i puntatori in quanto il tipo interfaccia non è istanziabile singolarmente.

#include <iostream>
#include <vector>

class Interface {
    
public:
    virtual std::string getString() = 0;
    
};

class A : public Interface {
    
    std::string getString() {
        return "A";
    }
    
};

class B : public Interface {
    
    std::string getString() {
        return "B";
    }
    
};

int main() {

    std::vector<Interface*> myVector = {new A, new B};
    
    for(auto i : myVector) {
        std::cout << i->getString() << std::endl; // Stampa prima "A" e poi "B"
    }
    
}

Chiamare il costruttore della classe padre

E' possibile dichiarare un costruttore nella classe figlio che chiama la classe padre tramite le liste di inizializzazione.

class A {
    A() {
        // Codice padre...
    }
};

class B : public A {
    B() : A() {
        // Codice figlio...
    }
}

Chiamare un metodo specifico

Se ho una classe che fa l'overload di un metodo della classe padre e ho bisogno di chiamare proprio il metodo del padre, oppure se in caso di ereditarietà multipla esiste una ambiguità tra due metodi con la stessa firma, allora posso specificare quale è la classe che contiene il metodo che voglio chiamare:

class A {
public:
    std::string getString() {
        return "A";
    }
};

class B : public A {
public:
    std::string getString() {
        return "B";
    }
};

int main() {
    B miaClasse;
    miaClasse.getString();    // Chiama il metodo di B
    miaClasse.A::getString(); // Chiama il metodo di A
}