Programmazione Orientata agli Oggetti in C++: Classi, Eredità e Polimorfismo

LT
Luca Terribili
Autore

Un oggetto è un'istanza concreta di una classe, con valori specifici per i suoi attributi. Per esempio, una classe Auto potrebbe contenere i campi marca, modello e anno, mentre i metodi accendi e guida descrivono le azioni che un’auto può compiere. Creare più oggetti di tipo Auto significa ottenere più veicoli, ognuno con le proprie caratteristiche.

Il vero potere della POO emerge quando combinano incapsulamento, ereditarietà, polimorfismo e astrazione. Queste caratteristiche permettono di modellare concetti reali in modo flessibile, di estendere funzionalità esistenti senza duplicare codice e di scrivere funzioni che operano su gruppi eterogenei di oggetti condividendo un’interfaccia comune.

Classi in C++: definizione e utilizzo

Una classe si dichiara con la parola chiave class seguita dal nome desiderato. All’interno è possibile definire attributi (membri di dati) e metodi (membri di funzione).

class Auto {
    // Attributi
    std::string marca;
    std::string modello;
    int anno;

    // Metodo
    void accendiMotore() {
        std::cout << "Il motore dell'auto è acceso." << std::endl;
    }
};

Per creare un oggetto basta istanziarlo con la sintassi NomeClasse nomeOggetto; e accedere ai membri tramite l’operatore punto (.).

Auto miaAuto;
miaAuto.marca = "Fiat";
miaAuto.modello = "Panda";
miaAuto.anno = 2010;
miaAuto.accendiMotore();

Attributi e metodi

Gli attributi descrivono lo stato interno dell’oggetto, mentre i metodi ne definiscono i comportamenti. La visibilità (public, private, protected) controlla l’accesso a questi membri: gli attributi privati sono modificabili solo da metodi della stessa classe, garantendo l’integrità dei dati.

class Persona {
private:
    std::string nome;
    int eta;
    std::string indirizzo;
public:
    void parla() {
        std::cout << "Ciao, sono " << nome << "!" << std::endl;
    }
    void cammina() {
        std::cout << nome << " sta camminando." << std::endl;
    }
};

Costruttori e distruttori

Il costruttore è un metodo speciale chiamato automaticamente al momento della creazione dell’oggetto; serve a inizializzare gli attributi. Può essere sovraccaricato per offrire diverse modalità di inizializzazione.

class Esempio {
    int valore;
public:
    Esempio(int v) : valore(v) {
        std::cout << "Costruttore chiamato" << std::endl;
    }
};

Il distruttore, preceduto da ~, viene eseguito quando l’oggetto esce dallo scope, consentendo di rilasciare risorse allocate dinamicamente.

class Esempio {
    int* ptr;
public:
    Esempio(int v) { ptr = new int(v); }
    ~Esempio() { delete ptr; }
};

Eredità

L’ereditarietà permette di creare una nuova classe (derivata) che estende o specializza una classe esistente (base). La classe derivata eredita gli attributi e i metodi della base, potendo aggiungere nuove funzionalità o modificare quelle esistenti.

class Animale {
protected:
    std::string nome;
    int eta;
public:
    Animale(std::string n, int e) : nome(n), eta(e) {}
    void mangia() { std::cout << nome << " sta mangiando." << std::endl; }
    void dorme()  { std::cout << nome << " sta dormendo." << std::endl; }
};

class Cane : public Animale {
public:
    Cane(std::string n, int e) : Animale(n, e) {}
    void abbaia() { std::cout << nome << " abbaia." << std::endl; }
};

int main() {
    Cane mioCane("Fido", 3);
    mioCane.mangia();
    mioCane.dorme();
    mioCane.abbaia();
}

Sovrascrittura di metodi e polimorfismo

Il polimorfismo si realizza dichiarando metodi virtual nella classe base e sovrascrivendoli (override) nelle classi derivate. In questo modo è possibile chiamare un metodo su un riferimento o puntatore di tipo base e ottenere l’implementazione specifica della classe reale dell’oggetto.

class Animale {
public:
    virtual void emettiSuono() {
        std::cout << "Verso generico." << std::endl;
    }
};

class Cane : public Animale {
public:
    void emettiSuono() override {
        std::cout << "Il cane abbaia." << std::endl;
    }
};

void eseguiSuono(Animale& a) {
    a.emettiSuono();   // chiamata polimorfica
}

Classi virtuali e puntatori a interfaccia

Le classi astratte (con metodi virtual puri) definiscono un’interfaccia comune senza fornire un’implementazione completa. I puntatori o riferimenti a queste classi consentono di scrivere codice generico che opera su qualunque tipo derivato.

class Forma {
public:
    virtual void disegna() = 0;   // metodo puro
};

class Cerchio : public Forma {
public:
    void disegna() override { std::cout << "Disegno un cerchio\n"; }
};

class Quadrilatero : public Forma {
public:
    void disegna() override { std::cout << "Disegno un quadrilatero\n"; }
};

void stampaForma(Forma* f) { f->disegna(); }

int main() {
    Cerchio c; Quadrilatero q;
    stampaForma(&c);
    stampaForma(&q);
}

Esempio pratico: gestione di una biblioteca

Il seguente esempio mostra come modellare una gerarchia di libri. La classe base Libro raccoglie i dati comuni, mentre le classi derivate (Romanzo, Saggio, LibroDiTesto) aggiungono informazioni specifiche.

class Libro {
protected:
    std::string titolo, autore;
    int anno;
public:
    Libro(std::string t, std::string a, int y) : titolo(t), autore(a), anno(y) {}
    std::string getTitolo() const { return titolo; }
    std::string getAutore() const { return autore; }
    int getAnno() const { return anno; }
};

class Romanzo : public Libro {
    std::string genere;
public:
    Romanzo(std::string t, std::string a, int y, std::string g)
        : Libro(t, a, y), genere(g) {}
    std::string getGenere() const { return genere; }
};

class Saggio : public Libro {
    std::string casaEditrice;
public:
    Saggio(std::string t, std::string a, int y, std::string c)
        : Libro(t, a, y), casaEditrice(c) {}
    std::string getCasaEditrice() const { return casaEditrice; }
};

class LibroDiTesto : public Libro {
    int numeroPagine;
public:
    LibroDiTesto(std::string t, std::string a, int y, int n)
        : Libro(t, a, y), numeroPagine(n) {}
    int getNumeroPagine() const { return numeroPagine; }
};

void stampaInfo(Libro* l) {
    std::cout << "Titolo: " << l->getTitolo() << '\n';
    std::cout << "Autore: " << l->getAutore() << '\n';
    std::cout << "Anno: "   << l->getAnno() << '\n';
    if (auto* r = dynamic_cast<Romanzo*>(l))
        std::cout << "Genere: " << r->getGenere() << '\n';
    else if (auto* s = dynamic_cast<Saggio*>(l))
        std::cout << "Casa editrice: " << s->getCasaEditrice() << '\n';
    else if (auto* t = dynamic_cast<LibroDiTesto*>(l))
        std::cout << "Pagine: " << t->getNumeroPagine() << '\n';
}

Riflessioni finali

La POO in C++ offre un set completo di strumenti per costruire software modulare, riutilizzabile e facile da mantenere. Le classi raggruppano dati e funzioni, l’ereditarietà consente di specializzare comportamenti, mentre il polimorfismo rende possibile scrivere codice generico che opera su diversi tipi di oggetti. Sebbene non sia l’unica metodologia disponibile, la sua capacità di modellare il mondo reale in modo intuitivo la rende una scelta privilegiata per progetti di qualunque dimensione. Continuare a esplorare questi concetti permette di approfondire le proprie competenze e di affrontare sfide sempre più complesse con sicurezza.