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... E
poi in questo modo ogni istanza della classe sarebbe sempre inizializzata con
valori prefissati, e la situazione sarebbe sostanzialmente quella di prima.
Il metodo corretto e` quello di fornire un costruttore che il
compilatore possa utilizzare quando una istanza della classe viene creata, in
modo che tale istanza sia sin dall'inizio in uno stato consistente. Un costruttore
altro non e` che un metodo il cui nome e` lo stesso di quello della classe,
che puo` avere dei parametri, ma che non restituisce alcun tipo (neanche void );
il suo scopo e` quello di inizializzare le istanze della classe:
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 (il nome del costruttore non e` cioe` esplicitamente mensionato, esso
e` implicito nel tipo dell'istanza); 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.
Un costruttore puo` eseguire compiti semplici come quelli dell'esempio, tuttavia
non e` raro che una classe necessiti di costruttori molto complessi, specie
se alcuni membri sono dei puntatori; in questi casi un costruttore puo` eseguire
operazioni quali allocazione di memoria o accessi a unita` a disco se si lavora
con oggetti persistenti.
In alcuni casi, alcune operazioni possono richiedere la certezza assoluta che
tutti o parte dei campi dell'oggetto che si vuole creare siano subito inizializzati
prima ancora che incominci l'esecuzione del corpo del costruttore; la soluzione
in questi casi prende il nome di lista di inizializzazione.
La lista di inizializzazione e` una caratteristica propria dei costruttori e
appare sempre tra la lista di argomenti del costruttore e il suo corpo:
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.
Nel caso appena visto l'importanza della lista di inizializzazione puo` non
essere evidente, lo sara` di piu` quando parleremo di oggetti composti e di
ereditarieta`.
Una classe puo` possedere piu` costruttori, cioe` i costruttori possono essere
overloaded, in modo da offrire diversi modi per inizializzare una istanza; in
particolare alcuni costruttori assumono un significato speciale:
- il costruttore di default
ClassName::ClassName() ;
- il costruttore di copia
ClassName::ClassName(ClassName& X) ;
- altri costruttori con un solo argomento;
Il costruttore di default e` particolare, in quanto e` quello che il compilatore
chiama quando il programmatore non utilizza esplicitamente un costruttore nella
dichiarazione di un oggetto:
#include < iostream >
using namespace std;
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;
};
int main(int, char* []) {
cout << "definizione di B... ";
MyClass B(1, 5); // MyClass(int, int) chiamato!
cout << "definizione di C... ";
MyClass C; // costruttore di default chiamato!
return 0;
}
Eseguendo tale codice si ottiene l'output:
definizione di B... costruttore Trace(int, int)
definizione di C... costruttore di default
Ma 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).
Il costruttore di copia invece viene invocato quando un nuovo oggetto va inizializzato
in base al contenuto di un altro; modifichamo la classe Trace in
modo da aggiungere i seguente costruttore di copia:
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.
Se il programmatore non definisce un costruttore di copia, ci pensa il compilatore.
In questo caso il costruttore fornito dal compilatore esegue una copia bit a
bit (non e` proprio cosi`, ma avremo modo di vederlo in seguito) degli attributi;
in generale questo e` sufficiente, ma quando una classe contiene puntatori e`
necessario definirlo esplicitamente onde evitare problemi di condivisione di
aree di memoria.
I principianti tendono spesso a confondere l'inizializzazione con l'assegnamento;
benche` sintatticamente le due operazioni sono simili, in realta` esiste una
profonda differenza semantica: l'inizializzazione viene compiuta una volta sola,
quando l'oggetto viene creato; un assegnamento invece si esegue su un oggetto
precedentemente creato. Per comprendere la differenza facciamo un breve salto
in avanti.
Il C++ consente di eseguire l'overloading degli operatori, tra cui quello per
l'assegnamento; come nel caso caso del costruttore di copia, anche per l'operatore
di assegnamento vale il discorso fatto nel caso che tale operatore non venga
definito esplicitamente. Il costruttore di copia viene utilizzato quando si
dichiara un nuovo oggetto e si inizializza il suo valore con quello di un altro;
l'operatore di assegnamento invece viene invocato successivamente in tutte le
operazioni che assegnamo all'oggetto dichiarato un altro oggetto. Vediamo un
esempio:
#include < iostream >
using namespace std;
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;
};
int main(int, chra* []) {
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;
return 0;
}
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.
Essi sono a tutti gli effetti dei veri e propri operatori di conversione di
tipo(vedi appendice A) che convertono il loro argomento in una istanza della
classe. Ecco una classe che fornisce diversi operatori di conversione:
class MyClass {
public:
MyClass(int);
MyClass(long double);
MyClass(Complex);
/* ... */
private:
/* ... */
};
int main(int, char* []) {
MyClass A(1);
MyClass B = 5.5;
MyClass D = (MyClass) 7;
MyClass C = Complex(2.4, 1.0);
return 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.
Solo l'untima dichiarazione puo` apparentemente sembrare diversa, in pratica
e` comunque la stessa cosa: si crea un oggetto di tipo Complex
e poi lo si converte (implicitamente) al tipo MyClass , infine viene
chiamato il costruttore di copia per inizializzare C . Per finire,
ecco un confronto tra costruttori e metodi (o normali funzioni) che riassume
quanto detto:
|
Costruttori |
Metodi |
Tipo restituito |
nessuno |
qualsiasi |
Nome |
quello della classe |
qualsiasi |
Parametri |
nessuna limitazione |
nessuna limitazione |
Lista di inizializzazione |
si |
no |
Overloading |
si |
si |
Altre differenze e similitudini verranno esaminate nel seguito.
|