Costruttori e distruttori

Questa sezione contiene:

Costruttori

Un membro costruttore è una funzione chiamata quando l'oggetto viene dichiarato o allocato dinamicamente. Il suo nome deve essere lo stesso della classe a cui appartiene, ma ci possono essere diversi costruttori sovraccarichi (vedi overloading delle funzioni), a patto che la loro lista di parametri sia differente. Un costruttore non ha valore di ritorno (non si deve specificare nemmeno void, il lato sinistro del nome nella definizione deve essere lasciato vuoto). Esso non può essere dichiarato static.

Esempio:

class Complex
{
  float i, r;
public:
  Complex();                 // Primo semplice costruttore
  Complex (float, float);    // Secondo costruttore sovraccaricato
};
Complex::Complex()
{
  i = 0;
  r = 0;
}
Complex::Complex (float ii, float rr)
{
  i = ii;
  r = rr;
}
Ma come facciamo a chiamare un costruttore? Questo dipende se stai usando l'operatore "new" o no. Senza "new":
Complex x();          // O per farla breve:
Complex x;            // Chiamata del primo costruttore
Complex y (2, 2);     // Chiamata del secondo costruttore
Devi solo mettere i parametri dopo il nome della variabile, e il corrispondente costruttore sarà chiamato. Con l'operatore "new", si fa così:
Complex *x = new Complex();         // O più brevemente
Complex *y = new Complex;           // Chiamata del primo costruttore
Complex *z = new Complex (2, 2);    // Chiamata del secondo costruttore
...
delete x;
delete y;
delete z;
Il compilatore genera del codice che fa le seguenti cose: Se gli oggetti sono dichiarati al di fuori di ogni funzione come variabili globali, i loro costruttori sono chiamati prima che la funzione "main" inizi. Ci sono due costruttori particolari (di default e di copia) che sono descritti in seguito.

Distruttori

Dopo aver visto la definizione di un costruttore, l'utilità di un distruttore appare ovvia: è chiamato quando l'oggetto viene rilasciato dalla memoria. Il suo nome è il nome della classe preceduto da ~ (tilde). Non ha nessun valore di ritorno (neanche void). Non prende parametri, dal momento che il programmatore non ha mai bisogno di chiamarlo direttamente.

Esempio:

class Complex
{
  float i, r;
public:
  Complex();                 // Primo semplice costruttore
  Complex (float, float);    // Secondo costruttore "overloaded"
  ~Complex();                // Distruttore
};
Complex::~Complex()
{
  printf ("Destructor called.\n");
}
...
{
  Complex *x = new Complex (5, 5);
  Complex y;
  ...
  delete x;                  // Chiamata del distruttore per x
}                            // Chiamata implicita del distruttore per y
Quando "cancelliamo" un oggetto con delete, ecco cosa facciamo: Con un oggetto statico, non possiamo determinare quando il distruttore sarà chiamato, perchè le regole del C++ non lo indicano. Sappiamo solo che teoricamente esso sarà chiamato.

Costruttore e distruttore di default

Quando non implementi il distruttore, viene usato un distruttore di default, che non fa nulla. Ma non appena fornisci codice per il distruttore, questo codice sarà utilizzato.

C'è anche un costruttore di default, e anche questo diventa inutilizzabile quando un qualsiasi altro costruttore è dichiarato. Puoi allora usare solo i costruttori che hai fornito.

class Complex
{
public:
  Complex (float, float);   // È dichiarato un solo costruttore
};
Dopo questa, l'unico modo per costruire un oggetto Complex è di dare 2 parametri al costruttore.
Complex x;           // Il compilatore rifiuterà questa riga
Complex y (2, 2);    // è ok chiamare il giusto costruttore
Complex z[10];       // Non permesso (continua a leggere)
Con un array di oggetti, dovremmo fornire gli argomenti del costruttore per ciascun elemento del vettore. Ma a causa della sintassi che deve uniformarsi con quella dei tipi nativi, un array di oggetti può essere dichiarato soltanto se l'oggetto ha un costruttore che non prende nessun argomento, o un solo argomento, ma non più di uno:
class Complex
{
public:
  Complex (float = 0, float = 0);
};

Complex v1[5] = { 0, 1, 2, 3, 4 };   // per ogni elemento il costruttore è
                                     // chiamato con gli argomenti (x, 0)
void Create (int n)
{
  Complex v2[n] = { 0, 1, 2};  // I primi tre elementi sono inizializzati
                               // con (x, 0), e gli altri con (0,0)
}
Ndt: notare che nel caso del vettore v1 i valori di inizializzazione 0, 1, 2, 3, 4 sono le parti immaginarie dei numeri complessi, perchè il primo argomento del costruttore Complex (float, float) (vedi sopra) era usato come parte immaginaria. Se volete che accada il contrario, dovete definire il costruttore così, invertendo l'ordine degli argomenti:
Complex::Complex (float rr, float ii)
{
  r = rr;
  i = ii;
}
Fate come preferite: infatti anche in matematica alcuni preferiscono scrivere i numeri complessi come 2j+1 altri come 1+2j (che è lo stesso). Altri in entrambe le forme, senza adottare una sintassi precisa. Sopra quando è scritto (x, 0) per il primo numero (x) si intende la parte immaginaria e per il secondo (0) quella reale, perciò la convenzione adottata è quella di passare i numeri complessi al costruttore nella forma (parte imm, parte real) del tutto simile a (parte imm)j + parte real.

Costruttore di Copia

Il costruttore di copia è l'altro costruttore di default. Come argomento prende solo un riferimento ad un altro oggetto della stessa classe. Quando nessun costruttore di copia è dichiarato, il compilatore ne usa uno di default che copia ogni campo dell'oggetto sorgente nell'oggetto destinazione e non fa nient'altro. Se ne dichiari uno, dovrai copiare tutti i campi che vuoi manualmente. Da notare l'uso della keyword "const", necessaria al compilatore per riconoscere un costruttore di copia.
class Vector
{
  int n;
  float *v;
public:
  Vector();
  Vector (const Vector &);
};
Vector::Vector()
{
  v = new float[100];
  n = 100;
}
Vector::Vector (const Vector &vector)
{
  n = vector.n;                         // Copia del campo n
  v = new float[100];                   // Crea un nuovo array
  for (int i = 0; i < 100; i++)
    v[i] = vector.v[i];                 // Copia l'array
}
I costruttori di copia sono necessari se effettui allocazione di memoria all'interno di un oggetto, come nell'esempio visto. In questo caso, non puoi semplicemente limitarti a copiare il puntatore (come fa il costruttore di copia di default), ma hai davvero bisogno all'atto della copia di allocare una parte della memoria, in modo che i valori associati ai due oggetti possano essere differenti. Quindi i costruttori di copia risolvono il problema della condivisione di memoria indesiderata che si avrebbe all'atto dell'assegnamento di un oggetto con parti dinamiche (cioè con membri puntatori).

Questo costruttore di copia è chiamato ognivolta che dichiari un oggetto di una classe e gli assegni un valore con una sola istruzione.

Vector a;                      // Costruttore vuoto
Vector b (a);                  // Costruttore di copia
Vector c = a;                  // Costruttore di copia (equivalente alla precedente istruzione)
Vector *d = new vector (a);    // Costruttore di copia
Puoi utilizzare il costruttore di copia anche con i tipi nativi, come int o char:
int a = 2;             // Assegna un valore
int b (2);             // Assegna un valore

Includere oggetti in altri oggetti

Quello che vogliamo fare è semplice. Vogliamo includere una classe come membro di unl'altra. La dichiarazione è piuttosto semplice:
class A
{
  int a;
public:
  A (int);
};
class B
{
  int b;
  A a_element;
public:
  B (int, int);
};
In questo esempio, stiamo includendo un oggetto della classe A in un oggetto della classe B. La cosa interessante è quello che succede quando chiamiamo il costruttore di B. Dopo che la memoria è stata allocata, il linguaggio stabilisce che deve essere chiamato il costruttore per ogni oggetto incluso, e solo dopo di ciò il costruttore dell'oggetto contenitore o figlio (cioè B). Ma quale costruttore di A verrà chiamato (nel caso ve ne sia più di uno), e come specificare altrimenti? Quando definisci il costruttore di B, devi specificare quale costruttore di A deve essere chiamato, e con quali argomenti. Ecco come si fa:
B::B (int aa, int bb) : a_element (aa)
{
  // Codice per il costruttore
  b = bb;
}
Ndt: in questo esempio non c'è un costruttore senza argomenti, perciò se non specifichiamo nella definizione di B quale costruttore chiamare, il compilatore tenterà di inserire una chiamata ad un costruttore senza argomenti per la classe A, cioè a A::A () ma poichè questo non è definito ci darà un errore. Ricordate infatti che abbiamo detto che il costruttore di default (che è senza argomenti) viene interdetto quando viene definito un altro costruttore.
Sperimentate questa situazione col vostro compilatore.

Note sintattiche: notare l'uso di ":" prima della chiamata al costruttore di A. Se molti costruttori devono essere chiamati, basta separare le loro chiamate con delle virgole (come nell'esempio sotto).
Nota che la sintassi per inizializzare l'oggetto contenuto nell'oggetto è all'atto della chiamata di un costruttore è la stessa usata nelle normali dichiarazioni (tipo int b (2);); solo il tipo della variabile da inizializzare viene omesso, in quanto già noto dalla definizione della classe.
Un altro modo più semplice per scrivere il costruttore di B è di usare i costruttori dei tipi nativi:

B::B (int aa, int bb) : a_element (aa), b (bb)
{
  // Codice per il costruttore
}
Quando viene chiamato il costruttore di copia di default di un oggetto contenente altri oggetti, sarà implicitamente chiamato anche il costruttore di copia di ciascuno degli oggetti contenuti. Nel caso abbiamo bisogno di sovrapporre il costruttore di copia dell'oggetto contenitore, dobbiamo chiamare noi esplicitamente il costruttore di copia per ognuno degli oggetti contenuti: la sintassi per chiamare questi costruttori è la stessa usata nell'esempio precedente.

Ndt: ok, ok eccovi un esempio chiarificatore:

#include <stdio.h>

class A
{
public:
  A()
  {
    printf("costruttore vuoto di A chiamato\n");
  }
  A (const A&)
  {
    printf("costruttore di copia A chiamato\n");
  }
};

class B
{
  A aA;
public:
  B()
  {
    printf("costruttore vuoto di B chiamato\n");
  }
  B (const B& b) : aA(b.aA)     // provare ad eliminare : aA(b.aA)
  {
    printf("costruttore di copia B chiamato\n");
  }
};

main()
{
  B aB;
  B anotherB(aB);
}
senza : aA(b.aA) il costruttore di copia di A non viene chiamato e nel membro aA dell'oggetto anotherB non viene copiato l'oggetto membro aA dell'oggetto aB. Piuttosto viene richiamato il costruttore vuoto A() sul membro aA di anotherB (provare per credere):

output con la riga B (const B& b) : aA(b.aA)
costruttore vuoto di A chiamato
costruttore vuoto di B chiamato
costruttore di copia A chiamato
costruttore di copia B chiamato

output con la riga B (const B& b)
costruttore vuoto di A chiamato
costruttore vuoto di B chiamato
costruttore vuoto di A chiamato
costruttore di copia B chiamato


C++