|
Distruttori
Poiche` ogni oggetto ha una propria durata (lifetime) e` necessario disporre
anche di un metodo che permetta una corretta distruzione dell'oggetto stesso,
un distruttore.
Un distruttore e` un metodo che non riceve parametri, non ritorna alcun tipo
(neanche void) ed ha lo stesso nome della classe preceduto da una
~ (tilde):
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 A
Si 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 >
using namespace std;
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.
Solitamente il distruttore e` chiamato implicitamente dal compilatore quando
un oggetto termina il suo ciclo di vita, oppure quando un oggetto allocato con
new viene deallocato con delete:
void func() {
Trace A(5.5);
// chiamata costruttore
Trace* Ptr=new Trace(4.2); //
chiamata costruttore
/* ... */
delete Ptr;
// chiamata al
// distruttore
}
// chiamata al
// distruttore per A
In 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.
Facciamo un esempio:
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;
TVarPtr = 0;
}
/* ... */
}
if (TVarPtr) delete TVarPtr;
}
L'uso del puntatore permette di capire se la variabile e` stata deallocata
(nel qual caso il puntatore viene posto a 0) oppure no (puntatore non nullo).
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
Si noti che poiche` un distruttore non possiede argomenti, non e` possibile
eseguirne l'overloading; ogni classe cioe` possiede sempre e solo un unico distruttore.
|