Operatori && e ||
Anche gli operatori di AND e OR logico possono essere
ridefiniti, tuttavia c'e` una profonda differenza tra quelli predefiniti e
quelli che l'utente puo` definire. La versione predefinita di entrambi
gli operatori eseguono valutazioni parziali degli argomenti: l'operatore
valuta l'operando di sinistra, ma valuta anche quello di destra solo quando
il risultato dell'operazione e` ancora incerto.
In questi esempi l'operando di destra non viene mai valutato:
int var1 = 1;
int var2 = 0;
int var3 = var2 && var1;
var3 = var1 || var2;
In entrambi i casi il secondo operando non viene valutato poiche` il valore
del primo e` sufficiente a stabilire il risultato dell'espressione.
Le versioni sovraccaricate definite dall'utente non si comportano in
questo modo, entrambi gli argomenti dell'operatore sono sempre valutati
(al momento in cui vengono passati come parametri).
Smart pointer
Pn operatore particolarmente interessante e` quello di
dereferenzazione -> il cui comportamento e` un po' difficile da
capire.
Se T e` una classe che ridefinisce -> (l'operatore di
dereferenzazione deve essere un funzione membro non statica) e Obj
e` una istanza di tale classe, l'espressione
Obj -> Field;
e` valutata come
(Obj.operator->()) -> Field;
Conseguenza di cio` e` che il risultato di questo operatore deve essere uno tra
- un puntatore ad una struttura o una classe che contiene un membro
Field;
- una istanza di un'altra classe che ridefinisce a sua volta l'operatore.
In questo caso l'operatore viene applicato ricorsivamente all'oggetto
ottenuto prima, fino a quando non si ricade nel caso precedente;
In questo modo e` possibile realizzare puntatori intelligenti (smart pointer),
capaci di eseguire controlli per prevenire errori disastrosi.
Pur essendo un operatore unario postfisso, il modo in cui viene trattato
impone che ci sia sul lato destro una specie di secondo operando; se volete
potete pensare che l'operatore predefinito sia in realta` un operatore binario
il cui secondo argomento e` il nome del campo di una struttura, mentre
l'operatore che l'utente puo` ridefinire deve essere unario.
L'operatore virgola
Anche la virgola e` un operatore (binario) che puo`
essere ridefinito. La versione predefinita dell'operatore fa si` che entrambi
gli argomenti siano valutati, ma il risultato prodotto e` il valore del secondo
(quello del primo argomento viene scartato). Nella prassi comune, la virgola
e` utilizzata per gli effetti collaterali derivanti dalla valutazione
delle espressioni:
int A = 5;
int B = 6;
int C = 10;
int D = (++A, B+C);
In questo esempio il valore assegnato a D e` quello ottenuto dalla
somma di B e C, mentre l'espressione a sinistra
della virgola serve per incrementare A. A sinistra della virgola
poteva esserci una chiamata di funzione (a patto che il valore restituito
fosse del tipo oppertuno), che serviva solo per alcuni suoi effetti collaterali.
Quanto alle parentesi, esse sono necessarie perche` l'assegnamento ha la
precedenza sulla virgola.
Questo operatore e` comunque sovraccaricato raramente.
Autoincremento e autodecremento
Ili operatori ++ e -- meritano un breve
accenno poiche` esistono entrambi sia come operatori unari prefissi che unari
postfissi.
Le prime versioni del linguaggio non consentivano di distinguere tra le due
forme, la stessa definizione veniva utilizzata per le due sintassi. Le
nuove versioni del linguaggi consentono invece di distinguere e usano
due diverse definizioni per i due possibili casi.
La forma prefissa prende un solo argomento: l'oggetto cui e` applicato, la
forma postfissa invece possiede un parametro fittizio in piu` di tipo int.
I prototipi delle due forme di entrambi gli operatori per gli interi sono
ad esempio le seguenti:
int operator++(int A); // caso ++Var
int operator++(int A, int); // caso Var++
int operator--(int A); // caso --Var
int operator--(int A, int); // caso Var--
Il parametro fittizio non ha un nome e non e` possibile accedere ad esso.
New e delete
Neanche gli operatori new e delete fanno
eccezione, anche loro possono essere ridefiniti sia a livello di classe
o addirittura globalmente.
Sia come funzioni globali che come funzioni membro, la new
riceve un parametro di tipo size_t che al momento della chiamata
e` automaticamente inizializzato con il numero di byte da allocare e deve
restituire sempre un void *; la delete invece riceve un
void * e non ritorna alcun risultato (va dichiarata void).
Anche se non esplicitamente dichiarate, come funzioni membro i due operatori
sono sempre static.
Poiche` entrambi gli operatori hanno un prototipo predefinito,
non e` possibile avere piu` versioni overloaded, e` possibile
averne al piu` una unica definizione globale e una sola definizione per classe come
funzione membro. Se una classe ridefinisce questi operatori (o uno dei due)
la funzione membro viene utilizzata al posto di quella globale per gli
oggetti di tale classe; quella globale definita (anch'essa eventualmente
ridefinita dall'utente) sara` utilizzata in tutti gli altri casi.
La ridefinizione di new e delete e` solitamente effettuata in
programmi che fanno massiccio uso dello heap al fine di evitarne una eccessiva
frammentazione e soprattutto per ridurre l'overhead globale introdotto
dalle singole chiamate. La ridefinizione di questi operatori richiede
l'inclusione del file new.h fornito con tutti i compilatori.
Ecco un esempio di new e delete globali:
void * operator new(size_t Size) {
return malloc(Size);
}
void operator delete(void * Ptr) {
free(Ptr);
}
Le funzioni malloc() e free() richiedono al sistema (rispettivamente)
l'allocazione di un blocco di Size byte o la sua deallocazione
(in quest'ultimo caso non e` necessario indicare il numero di byte).
Sia new che delete possono accettare un secondo parametro,
nel caso di new ha tipo void * e nel caso della delete
e` di tipo size_t: nel caso della new il secondo parametro
serve per consentire una allocazione di un blocco di memoria ad un
indirizzo specifico (ad esempio per mappare in memoria un dispositivo
hardware), mentre nel caso della delete il suo compito e` di fornire
la dimensione del blocco da deallocare (utile in parecchi casi). Nel caso
in cui lo si utilizzi, e` compito del programmatore supplire un valore per
il secondo parametro (in effetti solo per il primo parametro della
new e` il compilatore che fornisce il valore).
Ecco un esempio di new che utilizza il secondo parametro:
void * operator new(size_t Size, void * Ptr = 0) {
if (Ptr) return Ptr;
return malloc(Size);
}
Questa new permette proprio la mappatura in memoria di un dispositivo
hardware.
Per concludere c'e` da dire che allo stato attuale non e` possibile ridefinire
le versioni per array di new e delete, non potete quindi ridefinire
gli operatori new[ ] e delete[ ].
Conclusioni
Per terminare questo argomento restano da citare gli
operatori per la conversione di tipo e analizzare la differenza tra operatori
come funzioni globali o come funzioni membro.
Per quanto riguarda la conversione di tipo, si rimanda
all'appendice A.
Solitamente non c'e` differenza tra un operatore definito globalmente e uno
analogo definito come funzione membro, nel primo caso per ovvi motivi
l'operatore viene dichiarato friend nelle classi cui appartengono i
suoi argomenti; nel caso di una funzione membro, il primo argomento e`
sempre una istanza della classe e l'operatore puo` accedere a tutti i suoi membri,
per quanto riguarda l'eventuale secondo argomento puo` essere necessaria
dichiararlo friend nell'altra classe. Per il resto non ci sono differenze
per il compilatore, nessuno dei due metodi e` piu` efficiente dell'altro;
tuttavia non sempre e` possibile utilizzare una funzione membro, ad esempio
se si vuole permettere il flusso su stream della propria classe , e` necessario
ricorrere ad una funzione globale, perche` il primo argomento non e` una istanza
della classe:
class Complex {
public:
/* ... */
private:
float Re, Im;
friend ostream & operator<<(ostream & os, Complex & C);
};
ostream & operator<<(ostream & os, Complex & C) {
os << C.Re << " + i" << C.Im;
return os;
}
Adesso e` possibile scrivere
Complex C(1.0, 2.3);
cout << C;
Pagina precedente - Pagina successiva