new
e delete
Se ancora non conosci cos'è l'overloading di funzione fai un salto qui.
Riflettete che proprio questa è la conseguenza logica interessante del fatto che
gli operatori non sono nient'altro che delle funzioni, che del resto è una banale
particolarizzazione già nota a tutti i matematici.
Ripeto: se gli operatori sono funzioni, allora possiamo fare l'overloading degli operatori
per dargli nuovi ulteriori definizioni che si adattino alla nostre proprie classi, proprio
come facciamo l'overloading di funzioni.
Ma non dobbiamo dimenticarci che gli operatori hanno tutti un significato "naturale", e, anche se ovviamente possibile, non è una buona idea ad esempio definire l'operatore di addizione per moltiplicare vettori!
operator
seguita dall'
operatore stesso. Quando sovraccarichiamo un operatore, possiamo scegliere tra due strategie:
possiamo dichiarare gli operatori come funzioni friend oppure come
funzioni membro.
Non c'è una soluzione migliore in assoluto, dipende dall'operatore.
class Complex { float re, im; public: Complex (float r = 0, float i = 0) { re = r; im = i; } friend Complex operator+ (Complex &, Complex &); }; Complex operator+ (Complex &a, Complex &b) { return Complex (a.re + b.re, a.im + b.im); }
class Complex { float re, im; public: Complex (float r = 0, float i = 0) { re = r; im = i; } Complex operator+ (Complex &); }; Complex Complex::operator+ (Complex &a) { return Complex (re + a.re, im + a.im); }In entrambi i casi, gli operatori possono poi essere utilizzati nella stessa maniera con cui si usano con i tipi standard:
{ Complex a (1, 2); Complex b (3, 4); Complex c; ... c = a + b; // Chiama la funzione di addizione }
Operatore | Numero di parametri (nel conto va incluso l'oggetto nel caso di funzioni membro) | Nota |
---|---|---|
() | indeterminato | Devi usare una funzione membro. |
[] | 2 | Devi usare una funzione membro. |
-> | 2 | Devi usare una funzione membro. Fanne l'Overload solo se ne hai davvero bisogno. |
new delete | 1 | Per una classe, definiscili come funzioni membro (in tal caso sono automaticamente definite static). Possono anche essere definiti globalmente. |
++ -- | 1 | Pre-incremento (o pre-decremento). |
++ -- | 2 (2º inutilizzato) | Post-in/decremento (il 2º argomento è di tipo int e non viene usato, serve solo per distinguere le forme postfisse e prefisse). |
& | 1 | Pre-definito (ritorna "this"). Fai l'Overload solo se c'è davvero bisogno. |
+ - ! ~ * | 1 | |
(cast) | 1 | Vedi la descrizione seguente. |
* / % + - | 2 | |
<< >> | 2 | |
< <= > == != | 2 | |
& ^ || && | | 2 | |
= | 2 | Pre-definito (fa la copia dei membri). Devi usare una funzione membro. |
+= -= *= /= %= | 2 | |
&= ^= |= <<= >>= | 2 | |
, | 2 |
class Matrix { int array[10][10]; public: // ritorna un elemento della matrice (per riferimento, in modo da essere in grado // di modificarlo; ad esempio posso scrivere Matrix matrix; ... matrix(2, 3) = 5;) int &operator()(int x, int y) { return array[x][y]; } // ritorna la somma degli elementi di una colonna // Ndt: si fa la convenzione che il primo indice sia quello di colonna e quindi il secondo è quello di riga int operator()(int x) { int s=0; for (int i=0; i<10; i++) s+=array[x][i]; return s; } }; { Matrix matrix; ... int a = matrix (2, 2); // elemento (2,2) int b = matrix (2); // somma della seconda colonna }Quando usi l'operatore [], puoi ritornare un riferimento all'oggetto. Così sarai in grado non solo di leggere l'elemento, ma anche di scriverlo:
class Vector { int array[10]; public: int & operator[](int x) { return array[x]; } }; { Vector v; int a = v[2]; // ottiene il valore v[5] = a; // imposta il valore }
class Complex { float re, im; public: Complex (float r = 0, float i = 0) { re = r; im = i; } Complex & operator= (float f) { re = f; im = 0; return *this; } }; { Complex a (10, 12); a = 12; // Chiamata dell'operatore= }Infatti l'operatore = come convertitore di tipo ha queste limitazioni: non può essere usato ovunque, ad esempio non può essere usato per convertire argomenti prima della chiamata di una funzione o per convertire da tipo classe a tipo nativo, proprio perchè non abbiamo accesso alle classi astratte rappresentate dai tipi nativi per poterlo definire lì dentro!
Un modo migliore per effettuare le conversioni di tipo da classe a classe e per poter fare anche quelle da classe a tipo nativo è usare gli operatori di cast descritti nel prossimo paragrafo.
Ndt: Sperimentate con questo esempio:
class test { public: // test(int) {} // togli il commento iniziale per vedere che succede test & operator= (int) { // ... return *this; } }; void foo(const test & c) // oppure test c { } int main() { foo(2); // qui una temporanea di tipo test viene creata // e subito distrutta dopo il ritorno return 0; }Questo esempio mostra anche come i costruttori possono essere usati (anche implicitamente) come convertitori di tipo (da tipo nativo a classe o anche da classe a classe). Notare che nonostante venga creata una variabile temporanea e la si inizializzi, l'operatore=(int) non viene usato, in quanto non viene effettuato un assegnamento della variabile temporanea, perciò avrete un errore se il costruttore
test(int)
non c'è.
Insisto su questo punto: se ad es. scrivete test t=3;
la variabile t non viene creata e poi assegnata
ma bensì creata e inizializzata e viene chiamato il costruttore test(int)
e non
prima il costruttore vuoto e poi l'operator=(int)
.
test t=3;
infatti è lo stesso di test t(3);
.
Notate quindi che in C++ c'è differenza tra inizializzazione e assegnamento. Sono due concetti un po' diversi.
Ndt: da notare che facciamo restituire all'operatore= un riferimento all'oggetto appena
assegnato. Notare anche l'istruzione return *this; che deve esserci, a meno che non
definite void il tipo di ritorno (questo però non permette di concatenare
gli operatori di assegnamento come si fa in C con i tipi nativi; es.: a=b=c; a=b=8;).
Qual è la ragione per cui abbiamo preferito far restituire un riferimento,
anzichè un valore come nel prototipo Complex operator= (float f);
?
Giusto per evitare la chiamata del costruttore di copia di default per costruire un
oggetto temporaneo da ritornare.
Questo programma vi aiuterà a capire la differenza e a spiegare quello che avviene
nel caso di assegnamenti multipli. Vi invito a sperimentare ulteriormente col vostro compilatore,
per risolvere eventuali altri dubbi. Come potete vedere, un modo semplice per capire quali
funzioni vengono chiamate è quello di far stampare delle stringhe.
#include <stdio.h> class Complex { float re, im; public: Complex (float r = 0, float i = 0) { re = r; im = i; } Complex (const Complex& complex) { printf("costruttore di copia chiamato\n"); // fa il lavoro di quello di default re = complex.re; im = complex.im; } Complex operator=(const Complex& complex) { printf("operator=(Complex&) chiamato\n"); // fa il lavoro di quello di default re = complex.re; im = complex.im; return *this; // oppure complex } Complex /*&*/ operator= (float f) // provare a uncommentare il & { printf("operator=(float) chiamato\n"); re = f; im = 0; return *this; } }; int main() { Complex a (100, 200); Complex b (10, 20); Complex c (1, 2); c = b = a = 12; return 0; }
operator int()
dentro una classe,
saremo in grado di convertire un oggetto di questa classe in un valore intero.
Questo operatore sarà chiamato anche implicitamente, dal compilatore.
Devono essere delle funzioni membro. Il tipo di ritorno non va mai indicato, perchè è implicitamente noto (ovviamente è lo stesso del nome dell'operatore).
class Complex { float re, im; public: Complex (float r = 0, float i = 0) { re = r; im = i; } operator float() { return re; } }; void foo (float f) { printf ("%f\n", f); } { Complex a (10, 12); float b = (float) a; // Chiamata esplicita dell'operatore float() float c = a; // Chiamata implicita dell'operatore float() foo ((float) a); // Chiamata esplicita dell'operatore float() foo (a); // Chiamata implicita dell'operatore float() }Si possono usare gli operatori di cast anche per convertire oggetti in altri oggetti. La sintassi è la stessa, basta usare il nome della classe a cui convertire invece di
float
nell'esempio precedente.
new
e delete
new
e delete
, devi
preoccuparti di realizzare l'allocazione di memoria. Questa è la ragione per cui
non dovresti farlo, a meno che non sai esattamente quello che vuoi.
La loro dichiarazione deve essere:
#include <sys/types.h> #include <new.h> static void * operator new (size_t); static void operator delete (void *, size_t);