// 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.
Array
per la gestione di array dinamici di
double; le funzioni membro siano, oltre al costruttore e al
distruttore: contaElementi(), azzera(), raddoppia(), stampa();