// ex10_3_1.cpp #include <iostream.h> class Pari { int n; static unsigned int abs(int a); static bool pari(int a); public: Pari (int N = 0) { if (pari(N)) n = N; else n = 0; } int num() const { return n; } // funzione di accesso a n int operator+ (int a) const { return n + a; } int operator- (int a) const { return n - a; } int operator* (int a) const { return n * a; } int operator/ (int a) const { return n / a; } Pari operator+ (Pari a) const { return n + a.n; } Pari operator- (Pari a) const { return n - a.n; } Pari operator* (Pari a) const { return n * a.n; } Pari operator/ (Pari a) const { int t = n / a.n; if (pari(t)) return t; return 0; } }; // funzione statica che torna il valore assoluto dell'argomento unsigned int Pari::abs (int a) { if (abs(a) >= 0) return a; return -a; } // funzione statica che verifica se l'argomento e` pari bool Pari::pari (int a) { if (a % 2 == 0) return true; return false; } void main() { Pari p = 4, q = 8; int a = 3; cout << "p: " << p.num() << "\n"; cout << "q: " << q.num() << "\n"; cout << "a: " << a << "\n"; cout << "p+a: " << p+a << "\n"; cout << "p-a: " << p-a << "\n"; cout << "p*a: " << p*a << "\n"; cout << "p/a: " << p/a << "\n"; Pari t; t = p+q; cout << "p+q: " << t.num() << "\n"; t = p-q; cout << "p-q: " << t.num() << "\n"; t = p*q; cout << "p*q: " << t.num() << "\n"; t = p/q; cout << "p/q: " << t.num() << "\n"; }
output:
p: 4
q: 8
a: 3
p+a: 7
p-a: 1
p*a: 12
p/a: 1
p+q: 12
p-q: -4
p*q: 32
p/q: 0
Nascono due problemi, derivanti dal fatto che noi dobbiamo forzatamente
accedere i campi dati privati da una funzione membro della classe. Noi tutti
siamo abituati a considerare le operazioni, ad esempio, di somma e
moltiplicazione commutative; nell'esempio precedente in realà non è
così : se sommiamo un numero intero ad un numero pari, il compilatore
utilizza la funzione membro int
operator+
(int a)
, la
quale ha come primo argomento sottinteso *
this, ma se sommiamo un
numero pari ad un intero, il compilatore non può sapere in base a quali
regole effettuare l'operazione. Teoricamente, sarebbe necessario modificare la
classe int aggiungendo un membro del tipo int
operator+
(Pari p)
; ovviamente non possiamo fare nulla del genere, in quanto
non è possibile reimplementare i tipi primitivi (sarebbe davvero la
fine se fosse possibile, si provi ad immaginare perché). L'unica soluzione
allora è quella di utilizzare funzione friend.
Una funzione friend ( = ``amica'') è una funzione che non è membro di una classe ma ha accesso a tutti i campi privati di essa; naturalmente l'attributo di friend va dichiarato nella dichiarazione di classe10.3. Le funzioni friend, oltre a tale utile proprietà, sono del tutto identiche alle altre funzioni, per cui è possibile effettuare qualunque tipo di overloading, compreso quello degli operatori. Tale ultima possibilità è quella che ci interessa al fine di risolvere il nostro problema: se al posto di utilizzare funzioni membro definiamo funzione friend otteniamo un doppio scopo: accedere l'implementazione interna della classe e avere a dispozione una certa simmetria, che ci permette di conservare alcune proprietà fondamentali e intuitive degli operatori, come la commutatività.
// ex10_3_2.cpp #include <iostream.h> class Pari { // funzioni friend friend int operator+ (Pari p, int n) { return p.n + n; } friend int operator* (Pari p, int n) { return p.n * n; } friend int operator+ (int n, Pari p) { return operator+(p, n); } friend int operator* (int n, Pari p) { return operator*(p, n); } private: int n; static unsigned int abs(int a); static bool pari(int a); public: Pari (int N = 0) { if (pari(N)) n = N; else n = 0; } int num() const { return n; } // funzione di accesso a n }; unsigned int Pari::abs (int a) { /* ... */ } bool Pari::pari (int a) { /* ... */ } void main() { Pari p = 4; int a = 3; cout << "p: " << p.num() << "\n"; cout << "a: " << a << "\n"; cout << "p+a: " << p+a << "\n"; // 7 cout << "a+p: " << a+p << "\n"; // 7 cout << "p*a: " << p*a << "\n"; // 12 cout << "a*p: " << a*p << "\n"; // 12 }
Come si vede le funzioni friend rendono la sovrapposizione di certi operatori molto più intuitiva e semplice da utilizzare; conviene allora implementare la sovrapposizione di tutti gli operatori con funzioni friend? Certamente no; un buon compromesso è quello di sovrapporre tramite funzioni friend tutti gli operatori binari, tramite funzioni membro quelli unari. Non si tratta di una regola di vita vera e propria, ma di un consiglio che, a grandi linee, può andare bene; la cosa migliore è valutare caso per caso, cercando di utilizzare la strada più breve che arrivi più lontano, sempre cercando di immedesimarsi in un utilizzatore della nostra classe e mano a mano soddisfacendo le sue aspettative.
Il secondo problema di cui abbiamo implicitamente promesso la soluzione è il
seguente: visto che tutti gli operatori possono essere sovrapposti, perché
non sovrapporre anche gli operatori <<
e >>
in maniera tale da
potere gestire in entrata ed in uscita i nostri oggetti classe, senza
ricorrere a funzioni estremamente scomode e poco intuitive come la più volte
utilizzata stampa()
? Certamente effettuare l'overloading degli
operatori di shift è sicuramente la soluzione migliore per gestire
l'entrata/uscita dei nostri tipi dati; fino ad ora non abbiamo mai imboccato
questa strada appunto perché è necessario implementare le funzioni di
overloading dgli stream di entrata/uscita tramite funzione
friend. Sarebbe necessario dire qualche parola su come il C++
gestisce i flussi di dati, ma è ancora troppo presto, per cui ci
accontenteremo di fornire un semplice schema per l'entrata e l'uscita degli
oggetti classe.
// ex10_3_3.cpp #include <iostream.h> class Pari { friend ostream& operator<< (ostream& os, const Pari& p); friend istream& operator>> (istream& is, Pari& p); private: int n; static bool pari(int a); static unsigned int abs (int a); public: Pari (int N = 0) { if (pari(N)) n = N; else n = 0; } }; unsigned int Pari::abs (int a) { /* ... */ } bool Pari::pari (int a) { /* ... */ } ostream& operator<< (ostream& os, const Pari& p) { cout << p.n; return os; } istream& operator>> (istream& is, Pari& p) { int a; cin >> a; p = Pari(a); return is; } void main() { Pari p[3]; int i; for (i = 0; i < 3; i++) cin >> p[i]; for (i = 0; i < 3; i++) cout << p[i] << " "; }
esempio di output:
2 5 8
2 0 8
L'operatore di uscita è solitamente semplice da implementare; esso accetta
come primo argomento un riferimento allo stream di uscita, che nel nostro
programma di esempio è cout
, e come secondo argomento un riferimento
costante all'oggetto di tipo Pari
da stampare. Siccome la funzione è
friend essa ha libero accesso a tutti i campi dati, compresi quelli
protetti e privati, dell'oggetto p
. L'operatore di entrata è in
questo caso molto semplice, in quanto un numero pari è sempre un numero
intero; se l'oggetto diventa invece più complesso è necessario inserire un
gran numero di controlli sui dati per evitare che, a causa di un errore di
immissione, si possa perdere coerenza di implementazione. Si noti che il
secondo argomento consiste di un riferimento non costante a Pari
; è
ovvio datosi che l'operatore di entrata modifica per definizione gli oggetti
sui quali opera. Entrambe le funzioni ritornano un puntatore agli streams
stessi, per permettere la concatenazione degli operatori di shift, come
abbiamo mostrato nell'uscita di ex10_3_3.cpp
.
<<
, per permettere la stampa sugli
streams predefiniti, per tutte le classi fino ad ora presentate nelle quali
era presente la funzione stampa()