=
) in C++ è certamente uno dei più
importanti, al punto da meritare una trattazione separata per quanto riguarda
la sovrapposizione di esso in una classe. Il compilatore C++, nel caso non sia
presente un operatore di assegnamento, ne crea uno default, il quale non ha
altro compito che copiare membro a membro la struttura dati
dell'oggetto a destra di =
in quello a sinistra di esso. Vediamo un
esempio:
// ex9_6_1.cpp #include <iostream.h> class A { int n; double x; public: A (int N, double X) { n = N; x = X; } void stampa () { cout << "(" << n << " , " << x << ")\n"; } // ~A (); // il distruttore non serve: // tutti i membri sono non dinamici }; void main() { A a (5, 2.71); a.stampa (); A* b = new A (-2, 3.14); b->stampa(); a = *b; a.stampa(); delete b; a.stampa(); }
output:
(5 , 2.71)
(-2 , 3.14)
(-2 , 3.14)
(-2 , 3.14)
Sebbene l'operatore di assegnamento non stato sovrapposto, non c'è davvero
nessun problema: tutti i campi di *b
vengono copiati in a
, la
quale è del tutto indifferente delle sorte di b
. Nel seguente esempio
però i problemi vengono fuori prepotentemente:
// ex9_6_2.cpp // ATTENZIONE: non funziona #include <iostream.h> class B { int* n; double* x; public: B (int N, double X) { n = new int(N); x = new double(X); } void stampa () { cout << "(" << *n << " , " << *x << ")\n"; } ~B () { // il distruttore serve: delete n; // ci sono membri sono dinamic delete x; } }; void main() { B a (5, 2.71); a.stampa (); B* b = new B (-2, 3.14); b->stampa(); a = *b; a.stampa(); delete b; a.stampa(); }
esempio di output:
(5 , 2.71)
(-2 , 3.14)
(-2 , 3.14)
(1075285264 , 3.14)
Segmentation fault
Cosa è cambiato in quest'ultimo esempio: i membri sono dinamici, per cui la
memoria ad essi destinata viene allocata nel costruttore e deallocata nel
distruttore (il quale è in questo caso fondamentale); i campi che vengono
copiati con l'operatore di assegnamento sono gli indirizzi dei due puntatori
n
e x
: gli indirizzi contenuti in tali campi vengono
semplicemente assegnati ai corrispondenti dell'oggetto a
, il quale
dunque non dealloca la memoria precedentemente occupata (producendo
garbage) e non alloca la memoria per i nuovi campi. Un disastro: basta
eliminare l'oggetto b
(o anche cambiare il valore dei campi di esso) e
il sistema va in crash (il messaggio di errore che mi è stato riportato è
``Segmentation fault'', ma potrebbe essere qualunque altra cosa).
La soluzione a questo problema è semplice: basta sovrapporre l'operatore di
assegnamento per tutte le classi che allocano memoria dinamica (praticamente
tutte quindi, se si escludono quelle costruite per scopi squisitamente
didattici). Il compito di tale operatore è dunque quello di deallocare la
memoria precedentemente allocata e di allocarne di nuova per i campi
assegnati; infine si possono copiare i valori dell'oggetto a destra
dell'operatore =
nella neonata struttura dati. Esso funge dunque da
distruttore e da costruttore insieme. Vediamo una possibile implementazione
dell'operatore di assegnamento per la classe ArrayS
, studiata nella
sezione precedente:
class ArrayS { /* ... */ public: /* ... */ const ArrayS& operator= (const ArrayS& a); }; const ArrayS& ArrayS::operator= (const ArrayS& a) { if (&a != this) { if (n != a.n) { n = a.n; delete[] array; array = new double[n]; } for (int i = 0; i < n; i++) array[i] = a.array[i]; } return *this; } |
Ci sono diverse importanti osservazioni:
a = a
; a tale fine basta inserire come primo statement
dell'operatore di assegnamento:
if (&a != this) { /* corpo della funzione */ } |
return
*this
;a
abbiano lo
stesso numero di elementi, tramite:
if (n != a.n) { /* dealloca e rialloca memoria */ } |
(a = b) = c; |
a = b = c; |
b
il valore di c
, e ad a
il valore
di ritorno di b = c
ovvero b
.Un semplice esempio che mostra l'utilizzo dell'operatore di assegnamento è il seguente 9.4:
// ex9_6_3.cpp #include "arrays2.h" void main() { ArrayS x(8),y(10); for (int i = 0; i < 10; i++) { if (i < 8) x[i] = i; y[i] = 10 - i; } cout << "\nx:\n"; x.stampa(5); cout << "\ny:\n"; y.stampa(5); x = y; cout << "\nx:\n"; x.stampa(5); }
output:
x:
0 | 1 | 2 | 3 | 4 | |
5 | 6 | 7 |
10 | 9 | 8 | 7 | 6 | |
5 | 4 | 3 | 2 | 1 |
10 | 9 | 8 | 7 | 6 | |
5 | 4 | 3 | 2 | 1 |
La filosofia di utilizzo dell'operatori di assegnamento è molto simile a
quella di un altra tipica funzione membro di una classe: il costruttore
di copia. Esso viene chiamato ogni volta che si ha la necessità di creare
un oggetto a partire da uno preesistente, ovvero nelle inializzazioni sulla
base di oggetti dello stesso tipo; il costruttore di copia (copy
constructor) è indicato solitamente con X::X(X&)
. Le operazioni da
eseguire all'interno di esso sono formalmente identiche a quelle
dell'operatore di assegnamento, con l'unica importante differenza che
non è necessario deallocare memoria: gli oggetti da inizializzare non
esistono prima di tale operazione. Si faccia bene attenzione che, supponendo
che x1
sia un oggetto di tipo X
, lo statement:
X* x2 = new X(x1); |
x1
con i dati di x2
; non
ci si faccia trarre in inganno dal simbolo =
: non si tratta di un
assegnamento!
Vediamo un esempio con la solita classe ArrayS
9.5:
class ArrayS { /* ... */ public: /* ... */ ArrayS (const ArrayS& a); }; ArrayS::ArrayS (const ArrayS& a) { n = a.n; array = new double[n]; for (int i = 0; i < n; i++) array[i] = a.array[i]; }
Il costruttore di copia, come ogni costruttore, viene chiamato automaticamente
dal C++, non è possibile cioè invocarlo esplicitamente come l'operatore di
assegnamento, o qualunque altra funzione membro. Se il costruttore di copia
non è presente, esso viene automaticamente generato dal compilatore; in tal
caso l'unica operazione compiuta è la copia membro a membro dei dati della
classe; come l'operatore di assegnamento e il distruttore, il costruttore di
copia è dunque necessario solo se la classe alloca memoria dinamica al suo
interno. Tali speciali funzioni membro, qualora non fosse necessaria la loro
esistenza, è bene dichiararle commentate, come abbiamo visto per il
distruttore nella sezione precedente. Un esempio del costruttore di copia per
ArrayS
è il seguente:
// ex9_6_4.cpp #include "arrays2.h" void main() { int n; cout << "quanti elementi? "; cin >> n; ArrayS x(n); for (int i = 0; i < n; i++) { cout << "? "; cin >> x[i]; } // copiamo il vettore x in un vettore y dinamico // ed in un vettore z non dinamico ArrayS y(x); ArrayS* z = new ArrayS(x); // ordiniamo i due vettori y e z z->ordina(); cout << "\ncopia del vettore:\n"; y.stampa(); cout << "\ncopia ordinata:\n"; z->stampa(); }
esempio di output:
quanti elementi? 4
? 2.71
? 3.14
? -1.4
? 5e-6
copia del vettore:
2.71 | 3.14 | -1.4 | 5e-06 |
3.14 | 2.71 | 5e-06 | -1.4 |
Un'ultima osservazione: l'operazione di copia di una classe potrebbe essere priva di senso; si pensi ad esempio ad una classe la quale rappresenti il sistema sul quale viene eseguito il programma: che senso avrebbe crearne una copia? In casi come questi, è possibile utilizzare uno stratagemma sottile: si dichiarino il costruttore di copia e l'operatore di assegnamento come funzioni membro private, senza definirle; sarà compito del compilatore inibire l'utilizzatore della nostra classe ad effettuare copie di un oggetto di tale classe. Si consideri il seguente esempio:
// ex9_6_5.cpp class Sistema { /* tanti dati */ Sistema (const Sistema&); const Sistema& operator= (const Sistema&); public: Sistema () { /* inizializzazione dei dati */ } ~Sistema () { /* distruzione delle variabili dinamiche */ } /* tante funzioni membro */ }; void main() { Sistema s; // Sistema s2(s); // no! il costruttore di copia e` privato Sistema s3; // s3 = s; // no! l'operatore di assegnamento e` privato }
RealePositivo
) che rappresenti un
numero reale positivo allocato dinamicamente; si sovrappongano gli
operatori di somma, prodotto, differenza, divisione, minore, minore-uguale;