Documenti di Didattica
Documenti di Professioni
Documenti di Cultura
(come il C) sono stati concepiti per realizzare applicazioni che non richiedano nel tempo piu` di poche modifiche. Al contrario i linguaggi a oggetti hanno come obiettivo l'estendibilita`, il programmatore e` in grado di estendere il linguaggio per adattarlo al problema da risolvere, in tal modo diviene piu` semplice modificare programmi creati precedentemente perche` via via che il problema cambia, il linguaggio si adatta. Famoso in tal senso e` stato FORTH, un linguaggio totalmente estensibile (senza alcuna limitazione), tuttavia nel caso di FORTH questa grande liberta` si rivelo` controproducente perche` spesso solo gli ideatori di un programma erano in grado di comprendene il codice. Anche il C++ puo` essere esteso, solo che per evitare i problemi di FORTH vengono posti dei limiti: l'estensione del linguaggio avviene introducendo nuove classi, definendo nuove funzioni e (vedremo ora) eseguendo l'overloading degli operatori; queste modifiche devono tuttavia sottostare a precise regole, ovvero essere sintatticamente corrette per il vecchio linguaggio (in pratica devono seguire le regole precedentemente viste e quelle che vedremo adesso).
ReturnType e` il tipo restituito (non ci sono restrizioni); @ indica un qualsiasi simbolo di operatore valido; ArgumentList e` la lista di parametri (tipo e nome) che l'operatore riceve, i parametri sono due per un operatore binario (il primo e` quello che compare a sinistra dell'operatore quando esso viene applicato) mentre e` uno solo per un operatore unario. Infine Body e` la sequenza di istruzioni
che costituiscono il corpo dell'operatore. Ecco un esempio di overloading di un operatore come funzione globale:
struct Complex { float Re; float Im; }; Complex operator+(const Complex& A, const Complex& B) { Complex Result; Result.Re = A.Re + B.Re; Result.Im = A.Im + B.Im; return Result; }
Si tratta sicuramente di un caso molto semplice, che fa capire che in fondo un operatore altro non e` che una funzione. Il funzionamento del codice e` chiaro e non mi dilunghero` oltre; si noti solo che i parametri sono passati per riferimento, non e` obligatorio, ma solitamente e` bene passare i parametri in questo modo (eventualmente utilizzando const come nell'esempio). Definito l'operatore, e` possibile utilizzarlo secondo l'usuale sintassi riservata agli operatori, ovvero come nel seguente esempio:
Complex A, B; /* ... */ Complex C = A + B;
L'esempio richiede che sia definito su Complex il costruttore di copia, ma come gia` sapete il compilatore e` in grado di fornirne uno di default. Detto questo il precedente esempio viene tradotto (dal compilatore) in
Complex A, B; /* ... */ Complex C(operator+(A, B));
Volendo potete utilizzare gli operatori come funzioni, esattamente come li traduce il compilatore (cioe` scrivendo Complex C = operator+(A, B) o Complex C(operator+(A, B))), ma non e` una buona pratica in quanto annulla il vantaggio ottenuto ridefinendo l'operatore. Quando un operatore viene ridefinito come funzione membro il primo parametro e` sempre l'istanza della classe su cui viene eseguito e non bisogna indicarlo nella lista di argomenti, un operatore binario quindi come funzione globale riceve due parametri ma come funzione membro ne riceve solo uno (il secondo operando); analogamente un operatore unario come funzione globale prende un solo argomento, ma come funzione membro ha la lista di argomenti vuota. Riprendiamo il nostro esempio di prima ampliandolo con nuovi operatori:
class Complex { public: Complex(float re, float im); Complex operator-() const; // - unario Complex operator+(const Complex& B) const; const Complex & operator=(const Complex& B); private: float Re;
float Im; }; Complex::Complex(float re, float im = 0.0) { Re = re; Im = im; } Complex Complex::operator-() const { return Complex(-Re, -Im); } Complex Complex::operator+(const Complex& B) const { return Complex(Re+B.Re, Im+B.Im); } const Complex& Complex::operator=(const Complex& B) { Re = B.Re; Im = B.Im; return B; }
La classe Complex ridefinisce tre operatori. Il primo e` il -(meno) unario, il compilatore capisce che si tratta del meno unario dalla lista di argomenti vuota, il meno binario invece, come funzione membro, deve avere un parametro. Successivamente viene ridefinito l'operatore + (somma), si noti la differenza rispetto alla versione globale. Infine viene ridefinito l'operatore di assegnamento che come detto sopra deve essere una funzione membro non statica; si noti che a differenza dei primi due questo operatore ritorna un riferimento, in tal modo possiamo concatenare piu` assegnamenti evitando la creazione di inutili temporanei, l'uso di const assicura che il risultato non venga utilizzato per modificare l'oggetto. Infine, altra osservazione, l'ultimo operatore non e` dichiarato const in quanto modifica l'oggetto su cui e` applicato (quello cui si assegna), se la semantica che volete attribuirgli consente di dichiararlo const fatelo, ma nel caso dell'operatore di assegnamento (e in generale di tutti) e` consigliabile mantenere la coerenza semantica (cioe` ridefinirlo sempre come operatore di assegnamento, e non ad esempio come operatore di uguaglianza). Ecco alcuni esempi di applicazione dei precedenti operatori e la loro rispettiva traduzione in chiamate di funzioni (A, B e C sono variabili di tipo Complex):
B C C C = = = = -A; A+B; A+(-B); A-B; // // // // // // B.operator=(A.operator-()); C.operator=(A.operator+(B)); C.operator=(A.operator+(B.operator-())) errore! complex& Complex::operator-(Complex&) non definito.
L'ultimo esempio e` errato poiche` quello che si vuole utilizzare e` il meno binario, e tale operatore non e` stato definito. Passiamo ora ad esaminare con maggiore dettaglio alcuni operatori che solitamente svolgono ruoli piu` difficili da capire.
che e` equivalente a
A = (B = (C = < Valore >));
Non lo si confonda con il costruttore di copia: il costruttore e` utilizzato per costruire un nuovo oggetto inizializzandolo con il valore di un altro, l'assegnamento viene utilizzato su oggetti gia` costruiti.
Complex C = B; /* ... */ C = D; // Costruttore di copia // Assegnamento
Un'altra particolarita` di questo operatore lo rende simile al costruttore (oltre al fatto che deve essere una funzione membro): se in una classe non ne viene definito uno nella forma X& X::operator=(const X&), il compilatore ne fornisce uno automaticamente. Lo standard stabilisce che sia il costruttore di copia che l'operatore di assegnamento forniti dal compilatore debbano eseguire non una copia bit a bit, ma una inizializzazione o assegnamento a livello di membri chiamando il costruttore di copia o l'operatore di assegnamento relativi al tipo di quel membro. In ogni caso comunque e necessario definire esplicitamente sia l'operatore di assegnamento che il costruttore di copia ogni qual volta la classe contenga puntatori, onde evitare spiacevoli condivisioni di memoria. Ci sono due buone norme da tenere presenti quando si ridefinisce l'operatore di assegnamento: 1. Evitare che un oggetto assegni a se stesso; 2. Ritornare un reference a *this. E` importante evitare autoassegnamenti, perche` se una operazione di assegnamento comporta la deallocazione di risorse, e` facile che un autoassegnamento porti l'oggetto in uno stato inconsistente:
class X { public: X& operator=(const X& rsh); private: char* Str; }; X& X::operator=(const X& rsh) { delete[] Str; // Se this==&rsh le due istruzioni successive // hanno comportamento non definito Str = new char[strlen(rsh.Str)+1]; strcpy(Str, rsh.Str); /* ... */
Inoltre e` importante ritornare un riferimento a *this perche` l'argomento dell'operatore e` un riferimento a costante, mentre il valore generalmente restituito e` un reference non const. Si osservi che eliminare il const dal parametro non e` una soluzione praticabile poiche` impedirebbe l'assegnamento di valori costanti:
Integer& Integer::operator=(Integer& rsh) { /* ... */ } Integer I = 3; // Errore!
Notate infine che, come per le funzioni, anche per un operatore e` possibile avere piu` versioni overloaded; in particolare una classe puo` dichiarare piu` operatori di assegnamento (da tipi diversi), ma e` quello di cui sopra che il compilatore fornisce quando manca.
dove T puo` essere anche un riferimento o un puntatore. Restituendo un riferimento l'espressione Arg1[Arg2][Arg3] viene tradotta in Arg1.operator[](Arg2).operator[](Arg3). Il seguente codice mostra un esempio di overloading di questo operatore:
class TArray {
public: TArray(unsigned int Size); ~TArray(); int operator[](unsigned int Index); private: int* Array; unsigned int ArraySize; }; TArray::TArray(unsigned int Size) { ArraySize = Size; Array = new int[Size]; } TArray::~TArray() { delete[] Array; } int TArray::operator[](unsigned int Index) { if (Index < ArraySize) return Array[Index]; else /* Errore */ }
Si tratta di una classe che incapsula il concetto di array per effettuare dei controlli sull'indice, evitando cosi` accessi fuori limite. La gestione della situazione di errore e` stata appositamente omessa, vedremo meglio come gestire queste situazioni quando parleremo di eccezioni. Notate che l'operatore di sottoscrizione restituisce un int e non e` pertanto possibile usare indicizzazioni multiple, d'altronde la classe e` stata concepita unicamente per realizzare array monodimensionali di interi; una soluzione migliore, piu` flessibile e generale avrebbe richiesto l'uso dei template che saranno argomento del successivo capitolo.