next up previous contents index
Next: L'oggetto this e i Up: Introduzione alle classi Previous: Protezione dei membri di   Indice   Indice analitico

Costruttori e distruttore

Quando creiamo un oggetto di tipo classe, così come per gli oggetti di tipo struttura, abbiamo bisogno di una particolare funzione che ci permetta di inizializzare il nostro oggetto: il costruttore. Essa viene automaticamente chiamata dal compilatore una ed una sola volta nella vita di un oggetto, al momento della definizione di esso. Nel caso non sia presente nessun costruttore allora ne viene generato automaticamente uno il quale non compie alcuna operazione; al contrario, se è presente almeno un costruttore allora non è possibile evitare l'inizializzazione dell'oggetto. Nel seguente esempio dichiariamo due classi; la prima di esse è priva di costruttori, la seconda ne ha uno.


// ex9_3_1.cpp

class A {
  int x, y;
};

class B {
  int x, y;
public:
  B (int X, int Y) {
    x = X;
    y = Y;
  }
};

void main() {
  A a;        // ok
  //  B b;    // no! esiste un costruttore con due argomenti
  B b (5, 7); // ok
}

Affinché si possano definire oggetti di tipo B senza passare alcun argomento, è necessario avere un costruttore default; esso può essere di tre tipi:

Si faccia attenzione al fatto che, nel caso si forniscano dei valori default agli argomenti di una funzione in una classe (in particolare di un costruttore), essi vanno specificati solo nella dichiarazione:


// ex9_3_2.cpp

class A {
  int x, y;
};

class B {
  int x, y;
public:
  B () {    // costruttore default senza argomenti
    x = 0; y = 0;
  }
};

class C {
  int x, y;
public:
  // costruttore default con due argomenti
  B (int X = 0, int Y = 0);
};

// C::C (int X = 0, int Y = 0) {  // no!
C::C (int X, int Y) {
  x = X;
  y = Y;
}

void main() {
  A a;  // x, y ?
  B b;  // x = 0, y = 0
  C c;  // x = 0, y = 0
  C d (3, 7);  // x = 3, y = 7
  C e (5);     // x = 5, y = 0
}

Il costruttore è il luogo nel quale avvengono generalmente tutte le inizializzazioni dei dati contenuti nell'oggetto classe; inoltre è comune utilizzare il costruttore per allocare i campi dinamici dell'oggetto. Supponiamo infatti di volere una classe contenente un array dinamico: non possiamo allocare memoria nella dichiarazione della classe, perché sappiamo che le dichiarazioni degli oggetti non allocano memoria; allora dobbiamo semplicemente dichiarare un puntatore come campo, ed inizializzarlo con l'indirizzo di un blocco di memoria nel costruttore. Vediamo il seguente esempio (che riprende ex9_2_2.cpp):


// ex9_3_3.cpp

class Voti {
public:
  Voti (int Max);  // costruttore
  bool inserisci (double v);
  double media ();
private:
  int max;  // massimo numero di voti
  int n;    // numero di voti inseriti
  double* voti;  // puntatore ad un array dinamico
};

Voti::Voti (int Max) {
  max = Max;
  voti = new double[max];
  n = 0;
}

bool Voti::inserisci (double v) {
  if (n >= max)
    return false;
  voti[n++] = v;
}

double Voti::media () {
  double somma = 0;
  for (int i = 0; i < n; i++)
    somma += voti[i];
  return somma / n;
}

void main() {
  // voti relativi all'anno scolastisco 1998/99
  Voti as9899 (30);
  as9899.inserisci (7);    as9899.inserisci (5.5);
  as9899.inserisci (6.5);  as9899.inserisci (7);
  as9899.media();  // 6.5
}

Se allochiamo della memoria nello heap in un costruttore, c'è un piccolo problema: quando viene deallocata tale memoria? La risposta è mai, ovvero solo al termine del programma e non prima. Affinché la memoria occupata da un oggetto classe possa essere liberata, una volta che l'oggetto non è più utilizzabile, dobbiamo inserire un'altra particolare funzione: il distruttore. Il nome di tale funzione deve essere lo stesso della classe, preceduto tuttavia da una tilde (`~'), ed essa deve essere priva di argomenti e di valore di ritorno; in una stessa classe può esserci al più un distruttore; nel caso esso non sia presente, come negli esempio fino ad ora considerati, ne viene generato uno dal compilatore il quale non effettua alcuna operazione. Normalmente il distruttore viene definito solo se ce n'è davvero bisogno, cioè solo se si alloca della memoria dinamica all'interno di un oggetto. Ad esempio la classe Voti appena utilizzata è meglio implementata nel seguente modo:


class Voti {
public:
  Voti (int Max);  // costruttore
  bool inserisci (double v);
  double media ();
  ~Voti ();        // distruttore
private:
  int max;  // massimo numero di voti
  int n;    // numero di voti inseriti
  double* voti;  // puntatore ad un array dinamico
};

Voti::~Voti () {
  delete[] voti;
}

Così come il costruttore viene automaticamente chiamato al momento della definizione di un oggetto, il distruttore subisca la stessa sorte al momento della distruzione di un oggetto, la quale avviene in due modi: l'oggetto classe non è più utilizzabile (ad es. perché viene definito all'interno di una funzione, o di un blocco) oppure l'oggetto era stato allocato dinamicamente e viene distrutto tramite delete. Si noti che non è necessario il distruttore se la classe non alloca memoria nello heap, ovvero se le variabili di essa sono tutte automatiche; in tal caso, è buona norma inserire nella classe la dichiarazione del costruttore commentata, per indicare ad un utilizzatore del nostro codice che il distruttore è volutamente non presente.

Un tipico esempio di classe che alloca memoria dinamica è la seguente:


// sstringa.h
// semplice classe per la gestione di una stringa
#include <iostream.h>

class SStringa {
public:
  SStringa (const char* s1 = 0);  // costruttore default
  // torna il numero di caratteri della stringa
  int lunghezza () { return n; }
  // modifica la stringa
  void modifica (const char* s1);
  // stampa la stringa sul cout
  void stampa () { cout << s; }
  ~SStringa ();  // distruttore
private:
  char* s; // stringa
  int n;   // lunghezza di s
};

// sstringa.cpp
#include "sstringa.h"
#include <string.h>

SStringa::SStringa (const char* s1) {
  n = strlen (s1);
  s = new char[n];
  strcpy (s, s1);
}

void SStringa::modifica (const char* s1) {
  int m = strlen (s1);
  if (n != m) {
    delete[] s;
    n = m;
    s = new char[n];
  }
  strcpy (s, s1);
}

SStringa::~SStringa () {
  delete[] s;
}

// ex9_3_4.cpp
#include "sstringa.h"
#include <iostream.h>

void main() {
  SStringa s1("ciao!");
  s1.stampa();
  cout << "\nlunghezza: " << s1.lunghezza() << "\n";
  s1.modifica("come stai?");
  s1.stampa();
  cout << "\nlunghezza: " << s1.lunghezza() << "\n";
}

output:
ciao!
lunghezza: 5
come stai?
lunghezza: 10

Per comodità abbiamo utilizzato due files separati per la nostra classe SStringa: uno d'intestazione (sstringa.h) e uno contenente le definizioni (sstringa.cpp); potremo così d'ora in poi utilizzare la classe SStringa come se fosse un tipo primitivo, semplicemente includendo il file di intestazione nel nostro progetto di compilazione. Si sarà notato sicuramente l'estrema brevità delle funzioni distruttore: in effetti il loro unico compito è nella maggior parte dei casi quello di deallocare la memoria occupata dinamicamente nella vita di un oggetto; vedremo in seguito che gli algoritmi di recupero della memoria nello heap possono essere anche più complicati di una semplice delete.

Prima di chiudere l'argomento sui distruttori, si studi l'output del seguente semplice programma:


// ex9_3_5.cpp
#include <iostream.h>
class A {
public:
  A () { cout << "A::A()\n"; }
  ~A () { cout << "A::~A()\n"; }
};

class B {
public:
  B () { cout << "B::B()\n"; }
  ~B () { cout << "B::~B()\n"; }
};

void main() {
  A a;
  B* b = new B;
}

esempio di output:
A::A()
B::B()
A:: A()

Il precedente esempio contiene due definizioni di classi (A e B) le quali sono praticamente identiche: entrambe non fanno altro che segnalare il momento in cui vengono chiamati i loro costruttori e distruttori. Come si vede, il distruttore della classe B non è stato chiamato: il compilatore ha deciso di liberare la memoria occupata da b in altra maniera alla fine del programma. Il compito di decidere a fine programma se chiamare o meno il distruttore di un oggetto è esclusivamente del compilatore; per cui non si facciano abusi dei distruttori: servono solo a deallocare la memoria in fase di esecuzione. Scrivere una classe del genere:


class Qualsiasi {
public:
  /* ... */
  ~Qualsiasi () { cout << "fine programma"; }
};

credendo che il compilatore, a fine programma, invochi il distruttore di un nostro oggetto dandoci la possibilità, ad esempio, di segnalare la fine del programma stesso all'utente, è del tutto inappropriato.

ex-2
si scriva una classe Array per la gestione di array dinamici di double; le funzioni membro siano, oltre al costruttore e al distruttore: contaElementi(), azzera(), raddoppia(), stampa();


next up previous contents index
Next: L'oggetto this e i Up: Introduzione alle classi Previous: Protezione dei membri di   Indice   Indice analitico
Claudio Cicconetti
2000-09-06