Menu

Costruttori in C++: inizializzare gli oggetti nel modo giusto

Un costruttore è la funzione membro speciale che viene eseguita quando si crea un oggetto. Scopri i costruttori di default, parametrizzati e di copia, le liste di inizializzazione dei membri e come evitare oggetti inizializzati a metà.

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

Cos'è un costruttore

Nella pagina precedente hai definito delle classi e hai dato loro delle variabili membro. Ma i membri di un oggetto appena creato contengono qualunque valore spazzatura ci fosse in quella memoria, a meno che tu non li imposti. Un costruttore risolve il problema: è una funzione membro speciale che viene eseguita automaticamente nel momento in cui un oggetto viene creato, e il suo unico compito è lasciare l'oggetto in uno stato valido e completamente inizializzato.

Un costruttore ha lo stesso nome della classe e nessun tipo di ritorno, nemmeno void. Non lo chiami mai direttamente: è il compilatore a chiamarlo per te ogni volta che un oggetto viene creato.

Counter() senza parametri si chiama costruttore di default: è quello usato quando crei un oggetto senza passare argomenti.

Costruttori parametrizzati

Un costruttore senza argomenti va bene, ma di solito vuoi creare un oggetto con valori specifici. Un costruttore parametrizzato accetta argomenti e li usa per inizializzare i membri.

Una classe può avere più di un costruttore, purché le liste dei parametri siano diverse: è il normale overloading delle funzioni applicato ai costruttori. Qui Point si può creare con o senza coordinate:

Un'insidia comune: Point p(); non crea un oggetto, perché il compilatore lo legge come la dichiarazione di una funzione chiamata p che restituisce un Point. Per chiamare il costruttore di default scrivi Point p; (senza parentesi) oppure Point p{}; con le graffe.

Liste di inizializzazione dei membri

Finora gli esempi assegnavano i membri dentro il corpo del costruttore. Per i tipi semplici funziona, ma è lo strumento sbagliato. Quando il corpo viene eseguito, ogni membro è già stato costruito di default; il corpo poi butta via quel valore e ci assegna sopra. Una lista di inizializzazione dei membri inizializza ogni membro direttamente, prima del corpo, in un solo passaggio.

La sintassi prevede i due punti dopo la lista dei parametri, seguiti da coppie member(value):

Per un membro string questo evita anche di costruire una stringa vuota e poi assegnarla: la lista di inizializzazione la costruisce correttamente al primo colpo.

La lista di inizializzazione non è solo un'ottimizzazione; è obbligatoria in tre casi, perché il corpo può solo assegnare, non inizializzare:

  • Membri const: non puoi assegnare un valore a un const dopo che esiste.
  • Membri riferimento: un riferimento deve essere legato nel momento in cui nasce.
  • Membri il cui tipo non ha un costruttore di default.
class Sensor {
    const int id;        // membro const
    int& slot;           // membro riferimento

public:
    Sensor(int sensorId, int& s) : id(sensorId), slot(s) {}
    // Provare ad assegnare id o slot nel corpo non compilerebbe.
};

Una sottigliezza da conoscere: i membri vengono inizializzati nell'ordine in cui sono dichiarati nella classe, non nell'ordine in cui li elenchi nella lista di inizializzazione. Se l'inizializzatore di un membro legge un altro membro, conta l'ordine di dichiarazione: far divergere i due ordini è una fonte classica di utilizzo di un valore non ancora inizializzato.

Argomenti predefiniti e costruttori delegati

Non servono sempre overload separati. Gli argomenti predefiniti permettono a un unico costruttore di coprire più casi: ometti un argomento e ci pensa il valore predefinito.

Fai attenzione a combinare un costruttore parametrizzato con valori predefiniti e un costruttore di default Point() separato: il compilatore non sa quale chiamare per Point p; e segnala un'ambiguità. Scegli un solo approccio.

Quando hai più costruttori che condividono una parte della configurazione, un costruttore delegato (C++11) permette a un costruttore di chiamarne un altro invece di ripetere la logica. "Deleghi" mettendo l'altro costruttore nella lista di inizializzazione:

Il costruttore di copia

Quando crei un oggetto come copia di un altro, passandolo per valore, restituendolo o scrivendo Foo b = a;, viene eseguito il costruttore di copia. La sua firma riceve un riferimento const allo stesso tipo:

ClassName(const ClassName& other);

Se non ne scrivi uno, il compilatore genera un costruttore di copia predefinito che copia ogni membro. Per le classi che contengono solo valori (int, stringhe, vector) è esattamente ciò che serve, e non dovresti scriverne uno tuo.

La grande insidia sta nel prossimo capitolo sulla memoria: se la tua classe possiede un puntatore grezzo a memoria heap, il costruttore di copia predefinito copia il puntatore, non i dati, quindi due oggetti finiscono per puntare alla stessa memoria ed entrambi proveranno a liberarla. È il bug del double free. La regola pratica è la Regola del Tre/Cinque: se scrivi un distruttore personalizzato, quasi sicuramente ti servono anche un costruttore di copia (e un'assegnazione per copia) personalizzati. Nel C++ moderno la soluzione più pulita è contenere un std::vector o uno smart pointer, così la copia generata dal compilatore funziona e basta.

Nota anche che ricevere il parametro per riferimento è obbligatorio, non facoltativo: un costruttore di copia che ricevesse l'argomento per valore dovrebbe copiare l'argomento per chiamare se stesso, una ricorsione infinita che non compila nemmeno.

Prossimo: Distruttori

Un costruttore prepara un oggetto; un distruttore lo smonta. Quando un oggetto esce dallo scope o viene eliminato, il suo distruttore viene eseguito automaticamente: il posto perfetto per rilasciare file, connessioni di rete o memoria heap che l'oggetto teneva. La prossima pagina spiega come funzionano i distruttori, quando esattamente scattano e come si abbinano ai costruttori per dare al C++ il potente schema RAII.

Domande frequenti

Cos'è un costruttore in C++?

Un costruttore è una funzione membro speciale con lo stesso nome della classe e nessun tipo di ritorno. Viene eseguito automaticamente quando si crea un oggetto, e il suo compito è portare l'oggetto in uno stato valido e completamente inizializzato prima che qualsiasi altro codice lo usi.

Qual è la differenza tra un costruttore di default e uno parametrizzato?

Un costruttore di default non riceve argomenti e si usa quando crei un oggetto senza fornire valori (Point p;). Un costruttore parametrizzato riceve argomenti, così chi lo chiama può inizializzare l'oggetto con valori specifici (Point p(3, 4);). Una classe può averli entrambi, perché i costruttori si sovraccaricano in base alla lista dei parametri.

Perché dovrei usare una lista di inizializzazione dei membri in C++?

Una lista di inizializzazione dei membri (: name(n), age(a)) inizializza i membri direttamente, prima che venga eseguito il corpo del costruttore. È obbligatoria per i membri const, i riferimenti e i membri senza costruttore di default, ed evita lo spreco di costruire di default e poi assegnare che avviene quando assegni dentro il corpo.

Illustrazione dei linguaggi di programmazione di Coddy

Impara a programmare con Coddy

INIZIA