Uso dei puntatori
I puntatori sono utilizzati sostanzialmente per tre
scopi:
#include < iostream.h > // Una lista e` composta da tante celle linkate // tra di loro; ogni cella contiene un valore // e un puntatore alla cella successiva. struct TCell { float AFloat; // per memorizzare un valore TCell * Next; // puntatore alla cella successiva }; // La lista viene realizzata tramite questa // struttura contenente il numero di celle // della lista e il puntatore alla prima cella struct TList { unsigned Size; // Dimensione lista TCell * First; // Puntatore al primo elemento }; void main() { TList List; // Dichiara una lista List.Size = 0; // inizialmente vuota int FloatToRead; cout << "Quanti valori vuoi immettere? " ; cin >> FloatToRead; cout << endl; // questo ciclo richiede e memorizza // nella lista valori reali for(int i=0; i < FloatToRead; ++i) { TCell * Temp = List.First; cout << "Creazione di una nuova cella..." << endl; List.First = new TCell; // new vuole il tipo di // variabile da creare cout << "Immettere un valore reale " ; cin >> List.First -> AFloat; cout << endl; List.First -> Next = Temp; // aggiunge la cella in testa alla lista ++List.Size; // Aggiorna la dimensione della lista } // il seguente ciclo calcola la somma // dei valori contenuti nella lista; // via via che recupera i valori, // distrugge le relative celle float Total = 0.0; for(int j=0; j < List.Size; ++j) { Total += List.First -> AFloat; TCell * Temp = List.First; // estrae la cella in List.First = List.First -> Next; // testa alla lista cout << "Distruzione della cella in testa alla lista..." << endl; delete Temp; // distrugge la cella estratta } cout << "Totale = " << Total << endl; }L'esempio mostra come creare e distruggere oggetti dinamicamente.
// alloca un array di 10 interi int * ArrayOfInt = new int [10]; // ora eseguiamo la deallocazione delete [] ArrayOfInt;Si noti inoltre che gli oggetti allocati nella
void Change(int * IntPtr) { *IntPtr = 5; }La funzione Change riceve come unico parametro un puntatore a int, ovvero un indirizzo di una cella di memoria; anche se l'indirizzo viene copiato in una locazione di memoria visibile solo alla funzione, la dereferenzazione di tale copia consente comunque la modifica dell'oggetto puntato:
int A = 10; cout << " A = " << A << endl; cout << " Chiamata della funzione Change(&A)... " << endl; Change(&A); cout << " Ora A = " << A << endl;l'output che il precedente codice produce e`:
A = 10 Chiamata della funzione Change(&A)... Ora A = 5Quello che nell'esempio accade e` che la funzione Change riceve l'indirizzo della variabile A e tramite esso e` in grado di agire sulla variabile stessa.
void Func(BigParam parametro); // funziona, ma e` meglio quest'altra dichiarazione void Func(const BigParam * parametro);Il secondo prototipo e` piu` efficiente perche` evita l'overhead imposto dal passaggio per valore, inoltre l'uso di const previene ogni tentativo di modificare l'oggetto puntato e allo stesso tempo comunica al programmatore che usa la funzione che non esiste tale rischio.
int * Sum(int a, int b) { int Result = a + b; return &Result; }Apparentemente e` tutto corretto e un compilatore potrebbe anche non segnalare niente, tuttavia esiste un grave errore: si ritorna l'indirizzo di una variabile locale. L'errore e` dovuto al fatto che la variabile locale viene distrutta quando la funzione termina e riferire ad essa diviene quindi illecito. Una soluzione corretta sarebbe stata quella di allocare Result nello heap e restituire l'indirizzo di tale oggetto (in questo caso e` cura di chi usa la funzione occuparsi della eventuale deallocazione dell'oggetto).
int Var = 5; float f = 0.5; int * IntPtr = &Var; int & IntRef = Var; // nei reference non e` necessario float & FloatRef = f; // usare & a destra di =Le ultime due righe dichiarano rispettivamente un riferimento di tipo int e uno di tipo float che vengono subito inizializzati usando le due variabili dichiarate prima; un riferimento va inizializzato immediatamente, e dopo l'inizializzazione non puo` essere piu` cambiato; si noti che non e` necessario utilizzare l'operatore & (indirizzo di) per eseguire l'inizializzazione. Dopo l'inizializzazione il riferimento potra` essere utilizzato in luogo della variabile cui e` legato, utilizzare l'uno o l'altro sara` indifferente:
cout << "Var = " << Var << endl; cout << "IntRef = " << IntRef << endl; cout << "Assegnamento a IntRef..." << endl; IntRef = 8; cout << "Var = " << Var << endl; cout << "IntRef = " << IntRef << endl; cout << "Assegnamento a Var..." << endl; Var = 15; cout << "Var = " << Var << endl; cout << "IntRef = " << IntRef << endl;Ecco l'output del precedente codice:
Var = 5 IntRef = 5 Assegnamento a IntRef... Var = 8 IntRef = 8; Assegnamento a Var... Var = 15 IntRef = 15Dall'esempio si capisce perche`, dopo l'inizializzazione, un riferimento non possa essere piu` associato ad un nuovo oggetto: ogni assegnamento al riferimento si traduce in un assegnamento all'oggetto riferito.
int * IntPtr = new int(5); // il valore tra parentesi specifica il valore cui // inizializzare l'oggetto allocato. Per adesso il // metodo funziona solo con i tipi primitivi. int & IntRef = *IntPtr;Si noti che il puntatore va dereferenziato, altrimenti si legherebbe il riferimento al puntatore (in questo caso l'uso del riferimento comporta implicitamente un conversione da int * a int).
double & DoubleRef = *new Double; // Ora si puo` accedere all'oggetto allocato // tramite il riferimento. DoubleRef = 7.3; // Di nuovo, e` compito del programmatore // distruggere l'oggetto crato con new delete &DoubleRef; // Si noti che va usato l'operatore &, per // indicare l'intenzione di deallocare // l'oggetto riferito, non il riferimento!L'uso dei riferimenti per accedere a oggetti dinamici e` sicuramente molto comodo perche` e` possibile uniformare tali oggetti alle comuni variabili, tuttavia e` una pratica che bisognerebbe evitare perche` puo` generare confusione e di conseguenza errori assai insidiosi.
void Esempio(Tipo * Parametro);oppure in modo del tutto equivalente
void Esempio(Tipo & Parametro);Naturalmente cambierebbe il modo in cui chiamare la funzione:
long double Var = 0.0; long double * Ptr = &Var; // nel primo caso avremmo Esempio(&Var); // oppure Esempio(Ptr); // nel caso di passaggio per riferimento Esempio(Var);In modo del tutto analogo a quanto visto con i puntatori e` anche possibile ritornare un riferimento:
double & Esempio(float Param1, float Param2) { /* ... */ double * X = new double; /* ... */ return *X; }Puntatori e reference possono essere liberamente scambiati, non esiste differenza eccetto che non e` necessario dereferenziare un riferimento.
&A + &Bnon si riuscirebbe a capire se si desidera sommare due indirizzi oppure i due oggetti (che potrebbero essere troppo grossi per passarli per valore). I riferimenti invece risolvono il problema eliminando ogni possibile ambiguita` e consentendo una sintassi piu` chiara.