🚀 Nuova versione beta disponibile! Feedback o problemi? Contattaci

Costruttori e Distruttori in C++

Codegrind Team•Aug 23 2024

In C++, i costruttori e i distruttori sono funzioni speciali che svolgono un ruolo cruciale nella gestione della vita di un oggetto. Mentre i costruttori vengono utilizzati per inizializzare un oggetto quando viene creato, i distruttori vengono chiamati quando un oggetto esce dal suo scope o viene distrutto, permettendo di liberare risorse allocate e di eseguire pulizie necessarie. In questo articolo, esploreremo come funzionano i costruttori e i distruttori, il loro utilizzo e le best practices per gestire risorse e inizializzazioni complesse.

Costruttori in C++

1. Costruttore di Default

Il costruttore di default è un costruttore senza parametri che viene chiamato automaticamente quando un oggetto viene istanziato senza argomenti. Se non viene definito esplicitamente, il compilatore genera un costruttore di default.

class MyClass {
public:
    MyClass() {
        std::cout << "Costruttore di default chiamato" << std::endl;
    }
};

MyClass obj; // Chiama il costruttore di default

2. Costruttore Parametrizzato

Il costruttore parametrizzato accetta uno o più argomenti, consentendo l’inizializzazione dell’oggetto con valori specifici.

class MyClass {
private:
    int x;

public:
    MyClass(int val) : x(val) {
        std::cout << "Costruttore parametrizzato chiamato con valore: " << x << std::endl;
    }
};

MyClass obj(10); // Chiama il costruttore parametrizzato

3. Lista di Inizializzazione dei Membri

La lista di inizializzazione dei membri permette di inizializzare i membri della classe prima che il corpo del costruttore venga eseguito. È particolarmente utile per inizializzare membri const, reference o per chiamare i costruttori base delle classi genitore in una gerarchia di ereditarietà.

class MyClass {
private:
    const int x;
    int& ref;

public:
    MyClass(int val, int& r) : x(val), ref(r) {
        std::cout << "Lista di inizializzazione utilizzata" << std::endl;
    }
};

4. Costruttore di Copia

Il costruttore di copia crea un nuovo oggetto come copia di un altro oggetto della stessa classe. Viene chiamato quando un oggetto viene passato per valore, restituito da una funzione o inizializzato con un altro oggetto dello stesso tipo.

class MyClass {
private:
    int* data;

public:
    MyClass(int val) : data(new int(val)) {}

    MyClass(const MyClass& other) : data(new int(*other.data)) {
        std::cout << "Costruttore di copia chiamato" << std::endl;
    }

    ~MyClass() {
        delete data;
    }
};

MyClass obj1(10);
MyClass obj2 = obj1; // Chiama il costruttore di copia

5. Costruttore di Move

Il costruttore di move trasferisce le risorse da un oggetto all’altro, piuttosto che copiarle. È utilizzato per evitare copie non necessarie e migliorare le performance quando si lavora con oggetti pesanti o risorse dinamiche.

class MyClass {
private:
    int* data;

public:
    MyClass(int val) : data(new int(val)) {}

    MyClass(MyClass&& other) noexcept : data(other.data) {
        other.data = nullptr;
        std::cout << "Costruttore di move chiamato" << std::endl;
    }

    ~MyClass() {
        delete data;
    }
};

MyClass obj1(10);
MyClass obj2 = std::move(obj1); // Chiama il costruttore di move

Distruttori in C++

1. Distruttore di Base

Il distruttore è una funzione speciale che viene chiamata automaticamente quando un oggetto esce dal suo scope o viene distrutto. Il distruttore ha lo stesso nome della classe, preceduto da una tilde (~), e non accetta parametri né restituisce valori.

class MyClass {
public:
    ~MyClass() {
        std::cout << "Distruttore chiamato" << std::endl;
    }
};

{
    MyClass obj; // Distruttore chiamato automaticamente alla fine del blocco
}

2. Liberazione delle Risorse

Il compito principale del distruttore è liberare le risorse allocate durante la vita dell’oggetto, come memoria dinamica, file aperti o connessioni di rete.

class MyClass {
private:
    int* data;

public:
    MyClass(int val) : data(new int(val)) {}

    ~MyClass() {
        delete data; // Libera la memoria dinamica
        std::cout << "Distruttore chiamato, memoria liberata" << std::endl;
    }
};

3. Distruttori in Classi Derivate

Quando si lavora con l’ereditarietà, i distruttori delle classi base e derivate vengono chiamati in ordine inverso di costruzione. È importante dichiarare il distruttore della classe base come virtual per garantire che il distruttore corretto venga chiamato in caso di polimorfismo.

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

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

Base* ptr = new Derived();
delete ptr; // Chiama correttamente il distruttore di Derived e poi di Base

Best Practices per Costruttori e Distruttori

1. Utilizzare la Lista di Inizializzazione dei Membri

Utilizza sempre la lista di inizializzazione dei membri per inizializzare variabili const, referenze e chiamare i costruttori delle classi base. Questo migliora la performance e previene l’uso di variabili non inizializzate.

2. Implementare il Distruttore Quando Necessario

Se la tua classe gestisce risorse dinamiche, come memoria allocata dinamicamente, file o connessioni, implementa sempre un distruttore per garantire che queste risorse vengano liberate correttamente.

3. Dichiarare il Distruttore Come Virtual in Classi Base

Se prevedi che la tua classe possa essere utilizzata come base per ereditarietà, dichiara il distruttore come virtual per garantire la corretta distruzione degli oggetti derivati.

4. Seguire la Regola dei Tre (e dei Cinque)

Se la tua classe necessita di un costruttore di copia, di un operatore di assegnazione o di un distruttore personalizzato, probabilmente avrai bisogno di tutti e tre. Con l’introduzione delle move semantics in C++11, la regola dei tre è diventata la regola dei cinque, includendo il costruttore di move e l’operatore di assegnazione move.

class MyClass {
public:
    MyClass(const MyClass& other);          // Costruttore di copia
    MyClass& operator=(const MyClass& other); // Operatore di assegnazione
    ~MyClass();                             // Distruttore
    MyClass(MyClass&& other) noexcept;      // Costruttore di move
    MyClass& operator=(MyClass&& other) noexcept; // Operatore di assegnazione move
};

Conclusione

I costruttori e i distruttori in C++ sono strumenti essenziali per la gestione della vita degli oggetti e delle risorse. Comprendere come utilizzarli correttamente ti permetterà di scrivere codice più robusto, efficiente e sicuro, riducendo il rischio di bug legati alla gestione della memoria e migliorando la manutenibilità del tuo software. Implementare costruttori e distruttori con attenzione alle best practices è fondamentale per sviluppare applicazioni C++ di alta qualità.