Costruttori
L'uso di un metodo Set() per eseguire
l'inizializzazione di un oggetto (come mostrato per la
struct Complex) e` poco elegante e alquanto insicuro: il programmatore
che usa la classe potrebbe dimenticare di chiamare tale metodo prima di cominciare
ad utilizzare l'oggetto appena dichiarato. Si potrebbe pensare di scrivere
qualcosa del tipo:
class Complex { public: /* ... */ private: float Re = 6; // Errore! float Im = 7; // Errore! };ma il compilatore rifiutera` di accettare tale codice. Il motivo e` semplice, stiamo definendo un tipo e non una variabile (o una costante) e non e` possibile inizializzare i membri di una classe (o di una struttura) in quel modo.
Class Complex { public: Complex(float a, float b) { // costruttore! Re = a; Im = b; } /* altre funzioni membro */ private: float Re; // Parte reale float Im; // Parte immaginaria };In questo modo possiamo eseguire dichiarazione e inizializzazione di un oggetto Complex in un colpo solo:
Complex C(3.5, 4.2);La definizione appena vista introduce un oggetto C di tipo Complex che viene inizializzato chiamando il costruttore con gli argomenti specificati tra le parentesi. Si noti che il costruttore non viene invocato come un qualsiasi metodo; un sistema alternativo di eseguire l'inizializzazione sarebbe:
Complex C = Complex(3.5, 4.2);ma e` poco efficiente perche` quello che si fa e` creare un oggetto Complex temporaneo e poi copiarlo in C, il primo metodo invece fa tutto in un colpo solo.
class Complex { public: Complex(float, float); /* ... */ private: float Re; float Im; }; Complex::Complex(float a, float b) : Re(a), Im(b) { }L'ultima riga dell'esempio implementa il costruttore della classe Complex; si tratta esattamente dello stesso costruttore visto prima, la differenza sta tutta nel modo in cui sono inizializzati i membri dato: la notazione Attributo(<Espressione>) indica al compilatore che Attributo deve memorizzare il valore fornito da Espressione; Espressione puo` essere anche qualcosa di complesso come la chiamata ad una funzione.
#include < iostream.h > class Trace { public: Trace() { cout << "costruttore di default" << endl; } Trace(int a, int b) : M1{a), M2(b) { cout << "costruttore Trace(int, int)" << endl; } private: int M1, M2; }; void main { cout << "definizione di B... "; MyClass B(1, 5); // MyClass(int, int) chiamato! cout << "definizione di C... "; MyClass C; // costruttore di default chiamato! }Eseguendo tale codice si ottiene l'output:
definizione di B... costruttore Trace(int, int) definizione di C... costruttore di defaultMa l'importanza del costruttore di default e` dovuta soprattutto al fatto che se il programmatore della classe non definisce alcun costruttore, automaticamente il compilatore ne fornisce uno (che pero` non da` garanzie sul contenuto dei membri dato dell'oggetto). Se non si desidera il costruttore di default fornito dal compilatore, occorre definirne esplicitamente uno (anche se non di default).
Trace::Trace(Trace& x) : M1(x.M1), M2(x.M2) { cout << "costruttore di copia" << endl; }e aggiungiamo il seguente codice a main():
cout << "definizione di D... "; Trace D = B;Cio` che viene visualizzato ora, e` che per D viene chiamato il costruttore di copia.
#include < iostream.h > class Trace { public: Trace(Trace& x) : M1(x.M1), M2(x.M2) { cout << "costruttore di copia" << endl; } Trace(int a, int b) : M1{a), M2(b) { cout << "costruttore Trace(int, int)" << endl; } Trace & operator=(const Trace& x) { cout << "operatore =" << endl; M1 = x.M1; M2 = x.M2; return *this; } private: int M1, M2; }; void main() { cout << "definizione di A... " << endl; Trace A(1,2); cout << "definizione di B... " << endl; Trace B(2,4); cout << "definizione di C... " << endl; Trace C = A; cout << "assegnamento a C... " << endl; C = B; }Eseguendo questo codice si ottiene il seguente output:
definizione di A... costruttore Trace(int, int) definizione di B... costruttore Trace(int, int) definizione di C... costruttore di copia assegnamento a C... operatore =Restano da esaminare i costruttori che prendono un solo argomento.
class MyClass { public: MyClass(int); MyClass(long double); MyClass(Complex); /* ... */ private: /* ... */ }; void main() { MyClass A(1); MyClass B = 5.5; MyClass D = (MyClass) 7; MyClass C = Complex(2.4, 1.0); }Le prime tre dichiarazioni sono concettualmente identiche, in tutti e tre i casi convertiamo un valore di un tipo in quello di un altro; il fatto che l'operazione sia eseguita per inizializzare degli oggetti non modifica in alcun modo il significato dell'operazione stessa; al piu` l'unica differenza e` che nel primo caso si esegue in un colpo solo conversione e inizializzazione, mentre nel secondo e nel terzo caso prima si esegue la conversione e poi si chiama il costruttore di copia.
Costruttori | Metodi | |
Tipo restituito | nessuno | qualsiasi |
Nome | quello della classe | qualsiasi |
Parametri | nessuna limitazione | nessuna limitazione |
Lista di inizializzazione | si | no |
Overloading | si | si |
class Trace { public: /* ... */ ~Trace() { cout << "distruttore ~Trace()" << endl; } private: /* ... */ };Il compito del distruttore e` quello di assicurarsi della corretta deallocazione delle risorse e se non ne viene esplicitamente definito uno, il compilatore genera per ogni classe un distruttore di default che chiama alla fine della lifetime di una variabile:
void MyFunc() { TVar A; /* ... */ } // qui viene invocato automaticamente il distruttore per ASi noti che nell'esempio non c'e` alcuna chiamata esplicita al distruttore, e` il compilatore che lo chiama alla fine del blocco applicativo (le istruzioni racchiuse tra { } ) in cui la variabile e` stata dichiarata (alla fine del programma per variabili globali e statiche). Poiche` il distruttore fornito dal compilatore non tiene conto di aree di memoria allocate tramite membri puntatore, e` sempre necessario definirlo esplicitamente ogni qual volta esistono membri puntatori; come mostra il seguente esempio:
#include < iostream.h > class Trace { public: /* ... */ Trace(long double); ~Trace(); private: long double * ldPtr; }; Trace::Trace(long double a) { cout << "costruttore chiamato... " << endl; ldPtr = new long double(a); } Trace::~Trace() { cout << "distruttore chiamato... " << endl; delete ldPtr; }In tutti gli altri casi, spesso il distruttore di default e` piu` che sufficiente e non occorre scriverlo.
void func() { Trace A(5.5); // chiamata costruttore Trace * Ptr = new Trace(4.2); // chiamata costruttore /* ... */ delete Ptr; // chiamata distruttore } // chiamata distruttore per AIn alcuni rari casi puo` tuttavia essere necessario una chiamata esplicita, in questi casi pero` il compilatore puo` non tenerne traccia (in generale un compilatore non e` in grado di ricordare se il distruttore per una certa variabile e` stato chiamato) e quindi bisogna prendere precauzioni onde evitare che il compilatore, richiamando il costruttore alla fine della lifetime dell'oggetto, generi codice errato.
void Example() { TVar B(10); /* ... */ if (Cond) B.~TVar(); } // Possibile errore!Si genera un errore poiche`, se Cond e` vera, e` il programma a distruggere esplicitamente B, e la chiamata al distruttore fatta dal compilatore e` illecita. Una soluzione al problema consiste nell'uso di un ulteriore blocco applicativo e di un puntatore per allocare nello heap la variabile:
void Example() { TVar TVarPtr = new TVar(10); { /* ... */ if (Cond) { delete TVarPtr; // l'uso di delete provoca prima una chiamata TVarPtr = 0; // a ~TVar e poi alla deallocazione della variabile } /* ... */ } if (TVarPtr) delete TVarPtr; }Comunque si tenga presente che i casi in cui si deve ricorrere ad una tecnica simile sono rari e spesso (ma non sempre) denotano un frammento di codice scritto male (quello in cui si vuole chiamare il distruttore) oppure una cattiva ingegnerizzazione della classe cui appartiene la variabile