Overloading degli operatori Il C++ consente all - TelPar

Download Report

Transcript Overloading degli operatori Il C++ consente all - TelPar

Appunti di OOP in C++
telpar.altervista.org
Overloading degli operatori
Il C++ consente all'utente di ridefinire gran parte dei suoi operatori anche su nuovi tipi definiti
dall'utente stesso (come le classi) è cioè assegnare a uno stesso operatore compiti diversi a seconda
del contesto in cui vengono invocati.
Tale operazione prende il nome di overloading ( o sovraccarico) degli operatori.
Il C++, purtuttavia, non consente la definizione di nuovi operatori, ma solo di ridefinire quelli
esistenti.
Gli operatori che non possono essere ridefiniti dall'utente sono:
.
.*
::
?:
sizeof
Il C++ già internamente a se stesso, fa overloading di diversi operatori. Ad esempio, fra i tanti:
“<<” è sia il simbolo dell'estrattore dallo stream sia l'operatore di shift a sinistra nelle operazioni bit
a bit; il compilatore, a secondo di come vengono invocati nel programma, sa come interpretarli. Es.:
std::cout << “Hello”; // stampa la stringa “hello”
1 << 2;
//shift a sx di due posizioni del bit 1
Un tipico esempio di ridefinizione degli operatori è quello dei numeri complessi. E' possibile, per
esempio, ridefinire l'operatore di estrazione dallo stream in modo che stampi un numero complesso
formattato secondo la notazione rettangolare. Es.
Complex z1(3,3);
std::cout << z1;
// produce la stampa di 3+j3
oppure ridefinire l'operatore di inserimento sullo stream per acquisire un numero complesso. Es:
Complex z2;
std::cin >> z2;
// stampa la richiesta a video della parte reale
// e della parte complessa
oppure della somma, del rapporto...Es.
Complex z1, z2, z3, z4;
z3=z1+z2;
z4=z3/z2;
Questa funzionalità del C++, dunque, è innanzitutto una comodità sintattica in quanto ci consente di
poter scrivere le espressioni in cui compaiono oggetti con la stessa compattezza e concisione tipica
delle espressioni in cui compaiono i tipi predefiniti del linguaggio.
Appunti di OOP in C++
telpar.altervista.org
MECCANISMI SINTATTICI
Per effettuare l'overloading di un operatore è sufficiente utilizzare una normale definizione di
funzione nella quale il nome della funzione è composto dalla parola chiave operator a cui viene
attaccata il simbolo dell'operatore. Esempio, nel caso della ridefinizione dell'operatore somma (+)
definiremo una funzione operator+
NUMERO DI PARAMETRI
Il numero di parametri da passare a una funzione di ridefinizione dipende da due fattori:
1. O l'operatore ridefinito è unario (es. !) e allora ci sarà un argomento oppure è un operatore
binario (es. +, /, -) e ci saranno due argomenti.
2. O l'operatore è ridefinito come funzione globale (un parametro se unario, due parametri se
binario) o come funzione membro (nessun argomento per gli unari, uno per I binari l'oggetto diventa l'argomento di sinistra).
Il discorso è coerente. Se la funzione di ridefinizione non è membro della classe allora ha bisogno
di un extraparametro esplicito, contenente l'handle all'oggetto su cui è invocata. Se, invece, la
funzione è membro l'extraparametro dell'handle è implicito, nascosto (punt. this).
Tale funzione di ridefinzione può essere una funzione membro pubblica, oppure una funzione
friend, se deve ispezionare/mutare le variabili membro; se, invece, essa può interagire con l'oggetto
avvalendosi di metodi pubblici, può anche essere una funzione globale, ma la soluzione “friend” è
senz'altro più efficiente perché accede direttamente alle variabili membro, senza passare attraverso
l'invocazione di altre funzioni che comportano costi in termini di efficienza.
TIPO DI RITORNO
In ogni caso, una funzione operatore (non condizionale) ritorna quasi sempre ritorna un oggetto (se deve
restituire un valore) oppure un reference dello stesso tipo dei due parametri sui quali si sta lavorando, se sono
del medesimo tipo, quando ha apportato delle modifiche all'oggetto.
Se la funzione crea un lvalue bisognerà qualificare il reference come non const, mentre se crea un rvalue
(come una costante) la qualifichiremo come const.
Le funzioni operatore condizionale, (es. == !) invece, ritornano solitamente un valore booleano.
FUNZIONE MEMBRO O FRIEND?
Vediamo quali sono i criteri di scelta di uno o dell'altro meccanismo d'accesso.
Se si effettua l'overloading di operatori quali ( ), [ ], -> o di qualsiasi altro operatore di
assegnamento (es. =) , la funzione operatore deve essere definita come membro della classe. Per
quanto riguarda tutti gli altri operatori, invece, le funzioni operatore possono anche essere funzioni
non membro.
Se la funzione operatore viene implementata come funzione membro, l'operando sinistro (o l'unico
previsto) deve essere un oggetto o un riferimento a un oggetto della classe dell'operatore. Se si ha
Appunti di OOP in C++
telpar.altervista.org
bisogno di porre nell'operando sinistro un oggetto diverso, la funzione operatore deve essere
necessariamente implementata come funzione non membro.
Una funzione non membro consente anche all'operatore ridefinito di essere commutativo.
OVERLOADING DEGLI OPERATORI DI ACCESSO ALLO STREAM
Ad esempio l'overloading dell'operatore << ha un operando sinistro di tipo std::ostream&
Infatti:
cout << oggetto.getStato;
per cui la funzione deve essere una funzione non membro.
ESEMPIO
Supponiamo di avere un oggetto di classe Data e di volerlo stampare nella forma GG/MM/AAAA.
class Data {
friend ostream& operator<< (ostream& out, Data& data);
public:
Data (int gg=1, int mm=1, int aaaa=1970);
private:
int g;
int m;
int a;
};
Data::Data()
{
g=gg;
m=mm;
a=aaaa;
}
ostream& operator<< (ostream& out, Data& data)
{
out << data.g << “/” << data.m << “/” << data.a;
return out;
}
int main() {
Data d1(12,11,2001);
cout << d1;
}
Appunti di OOP in C++
telpar.altervista.org
Quando invochiamo l'operatore su d1, il compilatore la trasforma in
operator<< (cout, d1);
Il passaggio è per reference, quindi il parametro formale out diviene un alias del parametro attuale
cout e il parametro formale data diventa un alias del parametro attuale d1.
Il tipo di ritorno è un riferimento a ostream in modo che I comandi possano essere concatenati e
cioè
cout << d1 << d2; // scrive concatenate due data
OVERLOADING DI OPERATORI UNARI
Per quanto riguarda l'overloading degli operatori unari bisogna operare sull'oggetto stesso, quindi se
si usa una funzione membro non ci sarà bisogno di alcun parametro (o meglio, c'è l'extraparametro
nascosto this), mentre se si usa una funzione non membro (es. una funzione friend) bisognerà
passare un riferimento all'oggetto (const dal momento che l'accesso è in sola lettura) oppure passare
una copia dell'oggetto; cioè, in entrambi i casi, dovremmo assicurarci che la funzione di
ridefinizione non possa modificare l'oggetto esterno.
Le funzioni devono essere non static, per poter accedere ai dati non static della classe.
E' preferibile utilizzare per questi operatori la funzione membro, che garantiscono l'incapsulamento
nella classe della loro implementazione.
ESEMPIO
class Stringa {
public:
bool operator!() const;
//...
};
// ritorna 1 se stringa vuota
OVERLOADING DI OPERATORI BINARI
Discorso analogo per gli operatori binari che possono essere ridefiniti come funzione membro,
sempre non static, con un argomento, se si tratta di funzioni membro, oppure con due argomenti, se
si tratta di una funzione friend, in cui uno dei due deve essere un riferimento a un oggetto della
classe. Ad esempio consideriamo la ridefinzione dell'operatore += per una classe Stringa che
aggiunga (concateni) un oggetto stringa a un'altro oggetto stringa
Se la funzione è membro, un parametro s (la stringa che vogliamo concatenare all'oggetto stringa
sul quale abbiamo invocato la funzione):
class Stringa {
public:
Appunti di OOP in C++
telpar.altervista.org
const String& operator+= ( const Stringa & s );
//..
};
L'invocazione sarà:
// Concatena s2 a s1 e ripone la stringa concatenata in s1
Stringa s1(“CIAO”), s2(“ MONDO”);
s1+=s2; // diventa... s1.operator+=(s2)
cout << s1; // stampa “CIAO MONDO”
Se la funzione, invece, è friend, due parametri s1 e s2 (le due stringhe che vogliamo concatenare); la
stringa che deve essere “attaccata” all'altro, per esempio s2, la qualifichiamo come const, non
dovendo essere modificata a differenza di s1)
class Stringa{
friend const Stringa& operator+=(Stringa& s1, const Stringa& s2);
};
// Concatena s2 a s1 e ripone la stringa concatenata in s1
Stringa s1(“CIAO”), s2(“ MONDO”);
s1+=s2; // diventa... operator+=(s1, s2)
cout << s1; // stampa “CIAO MONDO”
La differenza fra l'operatore + e += è che il primo ritorna un valore temporaneo che è un oggetto
paria alla somma di due oggetti (ma non modifica nessuno dei due oggetti) che può essere stampato,
inviato a un'altra funzione ecc., mentre il secondo operatore modifica l'oggetto su cui è stato
invocato modificandogli il valore pari alla somma dei due oggetti e ritorna un reference all'oggetto
modificato. Es. Supponiamo di voler costruire, per assurdo, una classe di numeri interi (non ce ne
sarebbe bisogno!), ma ci serve per capire.
class intero{
public:
intero (aa=0) : a(aa) {}; //costruttore
intero operator+(intero& x);
intero& operator+=(intero& x);
private:
int a;
}
intero intero::operator+ (intero& x)
{
return intero(a+x); //ritorna un intero temp. pari ad a+x }
intero& intero::operator+= (intero& x)
{
i=i+x; // modifica i aggiungedogli x
return *this; //ritorna un ref. All'oggetto (per le chiamate casc.)
}
Appunti di OOP in C++
telpar.altervista.org
LINEE GUIDA
Rob Murray, nel suo libro “C++ Strategies & Tatics”, Addison-Wesley a pag. 47 suggerisce di
seguire queste linee guida nella scelta fra funzione membro e non-membro:
Operatore
Tipo di funzione
raccomandata
Tutti gli operatori unari
membro
. = () [] -> ->*
deve essere membro
+= -= /= *= ^=
&= |=
%= >>=
<<=
membro
Tutti gli altri operatori binari
non-membr