Puntatori e reference


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.

Nelle dichiarazioni di puntatori bisogna prestare attenzione a diversi dettagli che possono essere meglio apprezzati tramite esempi:


  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;


A contribuire all'errore avra` sicuramente influito il fatto che l'asterisco stava attacato al nome del tipo, tuttavia cambiando stile il problema non si risolve piu` di tanto. La soluzione migliore solitamente consigliata e` quella di porre dichiarazioni diverse in righe diverse.

Ritorniamo all'esempio da cui siamo partiti.
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).
Attenzione: anche const, se utilizzato per dichiarare una costante puntatore, si applica ad un solo nome (come *) e valgono quindi le stesse raccomandazioni fatte sopra.

In alcuni casi e` necessario avere puntatori generici, in questi casi il puntatore va dichiarato void:


  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.



Operazioni sui puntatori

Dal punto di vista dell'assegnamento, una variabile di tipo puntatore si comporta esattamente come una variabile di un qualsiasi altro tipo primitivo, basta tener presente che il loro contenuto e` un indirizzo di memoria:


  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" ora
                         // puntano allo stesso oggetto


I 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.

L'operazione piu` importante che viene eseguita sui puntatori e quella di dereferenziazione o indirezione al fine di ottenere accesso all'oggetto puntato; l'operazione viene eseguita tramite l'operatore di dereferenzazione * posto prefisso al puntatore, come mostra il seguente esempio:


  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.
L'operatore * prefisso ad un puntatore seleziona l'oggetto puntato dal puntatore cosi` che *P utilizzato come operando in una espressione produce l'oggetto puntato da P.
Ecco quale sarebbe l'output del precedente frammento di codice se eseguito:


  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 = 30


L'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.
Quando si dereferenzia un puntatore bisogna prestare attenzione che esso sia stato inizializzato correttamente; la dereferenzazione di un puntatore inizializzato a 0 e` sempre un errore, la dereferenzazione di un puntatore non inizializzato causa errori non definiti (e potenzialmente difficili da scovare). Quando possibile comunque il compilatore segnala eventuali tentativi di dereferenziare puntatori che potrebbero non essere stati inizializzati tramite una warning.
Per i puntatori a strutture (o unioni) e` possibile utilizzare un altro operatore di dereferenzazione che consente in un colpo solo di dereferenziare il puntatore e selezionare il campo desiderato:


  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.
Sui puntatori e` definita una speciale aritmetica composta da somma e sottrazione. Se P e` un puntatore di tipo T, sommare 1 a P significa puntare all'elemento successivo di un ipotetico array di tipo T cui P e` immaginato puntare; analogamente sottrarre 1 significa puntare all'elemento precedente. E` possibile anche sottrarre da un puntatore un altro puntatore (dello stesso tipo), in questo caso il risultato e` il numero di elementi che separano i due puntatori:


  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 4


Sui puntatori sono anche definiti gli usuali operatori relazionali:


  <           minore di
  >           maggiore di
  <=          minore o uguale
  >=          maggiore o uguale
  ==          uguale a
  !=          diverso da




Puntatori vs array

Esiste una stretta somiglianza tra puntatori e array dovuta alla possibilita` di dereferenziare un puntatore nello stesso modo in cui si seleziona l'elemento di un array e al fatto che lo stesso nome di un array e` di fatto un puntatore al primo elemento dell'array:


  int Array[] = { 1, 2, 3, 4, 5 };
  int* Ptr = Array;       // equivale 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" e char* Ptr = "Una stringa" hanno lo stesso effetto, entrambe creano una stringa (terminata dal carattere nullo) il cui indirizzo e` posto rispettivamente in Array e in Ptr, e come mostra l'esempio un char* puo` essere utilizzato esattamente come un array di caratteri.
Esistono tuttavia profonde differenze tra puntatori e array: un puntatore e` una variabile a cui si possono applicare le operazioni viste sopra e che puo` essere usato come un array, ma non e` vero il viceversa, in particolare il nome di un array non e` un puntatore a cui e` possibile assegnare un nuovo valore (non e` cioe` modificabile). Ecco un esempio:


  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.



Pagina precedente - Pagina successiva



C++, una panoramica sul linguaggio - seconda edizione
© Copyright 1996-1999, Paolo Marotta