Oltre ai tipi primitivi visti
precedentemente, esistono altri due tipi fondamentali usati solitamente in
combinazione con altri tipi (sia primitivi che non): puntatori e
reference.
L'argomento di cui ora parleremo potra` risultare particolarmente complesso,
soprattuto per coloro che non hanno mai avuto a che fare con i puntatori: alcuni
linguaggi non forniscono affatto i puntatori (come il Basic, almeno in alcune
vecchie versioni), altri (Pascal) invece forniscono un buon supporto;
tuttavia il C++ fa dei puntatori un punto di forza (se non il punto di forza) e
fornisce un supporto ad essi persino superiore a quello fornito dal Pascal. E`
quindi caldamente consigliata una lettura attenta di quanto segue e sarebbe bene
fare pratica con i puntatori non appena possibile.
Puntatori
I puntatori possono essere pensati come maniglie da
applicare alle porte delle celle di memoria per poter accedere al loro contenuto sia
in lettura che in scrittura, nella pratica una variabile di tipo puntatore contiene
l'indirizzo di una locazione di memoria.
Vediamo alcune esempi di dichiarazione di puntatori:
short * Puntatore1; Persona * Puntatore3; double * * Puntatore2; int UnIntero = 5; int * PuntatoreAInt = &UnIntero;Il carattere * (asterisco) indica un puntatore, per cui le prime tre righe dichiarano rispettivamente un puntatore a short int, un puntatore a Persona e un puntatore a puntatore a double. La quinta riga dichiara un puntatore a int e ne esegue l'inizializzazione mediante l'operatore & (indirizzo di) che serve ad ottere l'indirizzo della variabile (o di una costante o ancora di una funzione) il cui nome segue l'operatore. Si osservi che un puntatore a un certo tipo puo` puntare solo a oggetti di quel tipo, (non e` possibile ad esempio assegnare l'indirizzo di una variabile di tipo float a un puntatore a char, come mostra il codice seguente), o meglio in molti casi e` possibile farlo, ma viene eseguita una coercizione (vedi appendice A):
float Reale = 1.1; char * Puntatore = &Reale; // errore!E` anche possibile assegnare ad un puntatore un valore particolare a indicare che il puntatore non punta a nulla:
Puntatore = 0;In luogo di 0 i programmatori C usano la costante NULL, tuttavia l'uso di NULL comporta alcuni problemi di conversione di tipo; in C++ il valore 0 viene automaticamente convertito in un puntatore NULL di dimensione appropriata.
float * Reale, UnAltroReale; int Intero = 10; const int * Puntatore = &Intero; int * const CostantePuntatore = &Intero; const int * const CostantePuntatoreACostante = &Intero;La prima dichiarazione contrariamente a quanto si potrebbe pensare non dichiara due puntatori a float, ma un puntatore a float (Reale) e una variabile di tipo float (UnAltroReale): * si applica solo al primo nome che lo segue e quindi il modo corretto di eseguire quelle dichiarazioni era
float * Reale, * UnAltroReale;La terza riga mostra come dichiarare un puntatore a un intero costante, attenzione non un puntatore costante; la dichiarazione di un puntatore costante e` mostrata nella penultima riga. Un puntatore a una costante consente l'accesso all'oggetto da esso puntato solo in lettura (ma cio` non implica che l'oggetto puntato sia effettivamente costante), mentre un puntatore costante e` una costante di tipo puntatore (a ...), non e` quindi possibile modificare l'indirizzo in esso contenuto e va inizializzato nella dichiarazione. L'ultima riga mostra invece come combinare puntatori costanti e puntatori a costanti per ottenere costanti di tipo puntatore a costante (intera, nell'esempio).
void * PuntatoreGenerico;I puntatori void possono essere inizializzati come un qualsiasi altro puntatore tipizzato, e a differenza di questi ultimi possono puntare a qualsiasi oggetto senza riguardo al tipo o al fatto che siano costanti, variabili o funzioni; tuttavia non e` possibile eseguire sui puntatori void alcune operazioni definite sui puntatori tipizzati.
int Pippo = 5, Topolino = 10; char Pluto = 'P'; int * Minnie = &Pippo; int * Basettoni; void * Manetta; // Esempi di assegnamento a puntatori: Minnie = &Topolino; Manetta = &Minnie; // "Manetta" punta a "Minnie" Basettoni = Minnie; // "Basettoni" e "Minnie" puntano ora // allo stesso oggettoI primi due assegnamenti mostrano come assegnare esplicitamente l'indirizzo di un oggetto ad un puntatore: nel primo caso la variabile Minnie viene fatta puntare alla variabile Topolino, nel secondo caso al puntatore void Manetta si assegna l'indirizzo della variabile Minnie (e non quello della variabile Topolino); per assegnare il contenuto di un puntatore ad un altro puntatore non bisogna utilizzare l'operatore &, basta considerare la variabile puntatore come una variabile di un qualsiasi altro tipo, come mostrato nell'ultimo assegnamento.
short * P; short int Val = 5; P = &Val; // P punta a Val (cioe` Val e *P sono // lo stesso oggetto); cout << "Ora P punta a Val:" << endl; cout << "*P = " << *P << endl; cout << "Val = " << Val << endl << endl; *P = -10; // Modifica l'oggetto puntato da P cout << "Val e` stata modificata tramite P:" << endl; cout << "*P = " << *P << endl; cout << "Val = " << Val << endl << endl; Val = 30; cout << "La modifica su Val si riflette su *P:" << endl; cout << "*P = " << *P << endl; cout << "Val = " << Val << endl << endl;Il codice appena mostrato fa si` che il puntatore P riferisca alla variabile Val, ed esegue una serie di assegnamenti sia alla variabile che all'oggetto puntato da P mostrandone gli effetti.
Ora P punta a Val: *P = 5 Val = 5 Val e` stata modificata tramite P: *P = -10 Val = -10 La modifica su Val si riflette su *P: *P = 30 Val = 30L'operazione di dereferenzazione puo` essere eseguita su un qualsiasi puntatore a condizione che questo non sia stato dichiarato void. In generale infatti non e` possibile stabilite il tipo dell'oggetto puntato da un puntatore void e il compilatore non sarebbe in grado di trattare tale oggetto.
Persona Pippo; Persona * Puntatore = &Pippo; Puntatore -> Eta = 40; cout << "Pippo.Eta = " << Puntatore -> Eta << endl;La terza riga dell'esempio dereferenzia Puntatore e contemporaneamente seleziona il campo Eta (il tutto tramite l'operatore ->) per eseguire un assegnamento a quest'ultimo. Nell'ultima riga viene mostrato come utilizzare -> per ottenere il valore di un campo dell'oggetto puntato.
int Array[10] = { 0, 1, 2, 3, 4, 5, 6, 7, 8, 9 }; int * P1 = &Array[5]; int * P2 = &Array[9]; cout << P1 - P2 << endl; // visualizza 4 cout << *P1 << endl; // visualizza 5 P1+=3; // equivale a P1 = P1 + 3; cout << *P1 << endl; // visualizza 8 cout << *P2 << endl; // visualizza 9 P2-=5; // equivale a P2 = P2 - 5; cout << *P2 << endl; // visualizza 4Sui puntatori sono anche definiti gli usuali operatori relazionali:
< minore di > maggiore di <= minore o uguale >= maggiore o uguale == uguale a != diverso da
int Array[ ] = { 1, 2, 3, 4, 5 }; int * Ptr = Array // equivalente a Ptr = &Array[0]; cout << Ptr[3] << endl; // Ptr[3] equivale a *(Ptr + 3); Ptr[4] = 7; // equivalente a *(Ptr + 4) = 7;La somiglianza diviene maggiore quando si confrontano array e puntatori a caratteri:
char Array[ ] = "Una stringa"; char * Ptr = "Una stringa"; // la seguente riga stampa tutte e due le stringhe // si osservi che non e` necessario dereferenziare un char * // (a differenza degli altri tipi di puntatori) cout << Array << " == " << Ptr << endl; // in questo modo, invece, si stampa solo un carattere: // la dereferenzazione di un char * o l'indicizzazione // di un array causano la visualizzazione di un solo carattere // perche` in effetti si passa all'oggetto cout non un puntatore a // char, ma un oggetto di tipo char (che cout tratta giustamente // in modi diversi) cout << Array[5] << " == " << Ptr[5] << endl; cout << *Ptr << endl;In C++ le dichiarazioni
char Array[ ] = "Una stringa"; char * Ptr = "Una stringa"; Array[3] = 'a'; // Ok! Ptr[7] = 'b'; // Ok! Ptr = Array; // Ok! Ptr++; // Ok! Array++; // errore, tentativo di assegnamento!In definitiva un puntatore e` piu` flessibile di quanto non lo sia un array, anche se a costo di un maggiore overhead.