4 - PUNTATORI
versione 15/01/2013
 
I PUNTATORI

Una variabile puntatore è una variabile che contiene l’indirizzo di memoria di un’altra variabile. In C++, per conoscere l’indirizzo di una variabile, è sufficiente far precedere al nome della variabile l’operatore &. Supponiamo che la variabile intera x, raffigurata nel riquadro di sinistra, sia allocata all’indirizzo di memoria 7500. con l'istruzione p_x=&x; la variabile p_x conterrà l’indirizzo della variabile x.


Per accedere al contenuto della cella il cui indirizzo è memorizzato in p_x è sufficiente far precedere alla variabile puntatore il carattere asterisco (*).

p_x = & x;
*p_x = 20;


Il C++ mette a disposizione un tipo particolare di inizializzazione di un puntatore. Infatti è possibile, ed anzi è buona norma farlo, inizializzare una variabile puntatore a NULL (ovvero non la si fa puntare a nessun indirizzo di memoria).
I puntatori e gli array sono argomenti strettamente correlati. Ricordiamo, infatti, che il nome di un array è in realtà un valore costante che rappresenta l’indirizzo del suo primo elemento.
float vArray[20];
float *ptArray;
ptArray=vArray;
// equivale a:
ptArray=&vArray[0];
Per poter capire a cosa serve un puntatore, è necessario sapere che la memoria del calcolatore in cui possono risiedere le variabili è suddivisa in due parti:

- lo Stack
- l’Heap

Nello Stack vengono immagazzinate tutte le variabili dichiarate nei programmi. Ad esempio quando si definisce int i; Il computer riserverà due byte di memoria dello Stack per la variabile i. Dichiarazioni di questo tipo sono abbastanza "statiche", in quanto non permettono di modificare il numero di byte assegnato ad una variabile o di deallocare la memoria di una variabile, quindi di liberare lo spazio precedentemente assegnato.
Quest’ultima osservazione può rappresentare un problema quando si manipolano grosse quantità di informazioni, in quanto potrebbe succedere che lo Stack si riempia e quando si tenta di allocare altra memoria si verifichi il cosiddetto Stack overflow (questo accade quando ad esempio una funzione ricorsiva entra in loop. Ad ogni chiamata vengono create le variabili locali della funzione ricorsiva e il processo continua fino a che non si esaurisce la memoria).

Per permettere al programmatore di utilizzare la memoria in maniera "dinamica" è possibile allora utilizzare la memoria Heap, detta anche memoria dinamica. Il termine dinamica sta proprio ad indicare che è data la possibilità al programmatore di allocare e deallocare la memoria a suo piacimento.
Esiste a tale scopo un’altra possibilità per inizializzare una variabile puntatore: utilizzando l’operatore new messo a disposizione dal C++. Le variabili puntatore inizializzate con l'operatore new oppure mediante calloc, malloc e realloc fanno riferimento alla memoria dinamica (heap)


Puntatori a funzione

In C++ (come in C) esistono i puntatori a funzione. Questi servono quando il programma deve scegliere quale funzione chiamare fra diverse possibili. La scelta non é definita a priori ma dipende dai dati del programma stesso. Questo processo si chiama late binding ("aggancio ritardato"): gli indirizzi delle funzioni da chiamare non vengono risolti al momento della compilazione, come avviene normalmente (early binding) ma al momento dell'esecuzione.
Il C tratta i nomi delle funzioni come se fossero dei puntatori alle funzioni stesse. Quindi, quando vogliamo assegnare ad un puntatore a funzione l'indirizzo di una certa funzione dobbiamo effettuare un operazione di assegnamento del nome della funzione al nome del puntatore a funzione. Un puntatori a funzione viene dichiarato in questo modo:

tipoR (*NomeFunz)(tipo1, tipo2, ... , tipoN );

Dichiara un puntatore a funzione NomeFunz che restituisce un puntatore a tipoR e che ha N argomenti di tipo: tipo1,...tipoN.
Le parentesi intorno al nome della funzione consentono la corretta interpretazione dell'istruzione.
Durante l'esecuzione del programma il puntatore a funzione deve essere assegnato (o inizializzato) con il nome di una funzione "vera", che deve essere precedentemente dichiarata con gli stessi argomenti e lo stesso tipo di valore di ritorno dichiarato nel puntatore a funzione. Continuando l'esempio precedente:

#include <stdio.h>
#include <stdlib.h>
int somma(int a, int b) { return a+b; }
int differenza(int a, int b) { return a-b; }
int prodotto(int a, int b) { return a*b; }
int main()
{
    int a, b, r;
    char op;
    int (*funz)(int,int);
    printf("Calcola: ");
    scanf("%d %c %d",&a,&op,&b);
    if (op=='+')
       funz=somma;
    else if (op=='-')
       funz=prodotto;
    else if (op=='*')
       funz=prodotto;
    else 
        exit(0);
    r=(*funz)(a,b); // utilizzo il puntatore alla funzione
    // potevo scrivere anche: r=funz(a,b);
    printf("%d %c %d = %d",a,op,b,r);
    // metto in stop il programma prima della sua chiusura
    fflush(stdin);
    getchar();
    return(0);
}
notare che i nomi delle funzioni e del puntatore vanno indicati da soli, senza i loro argomenti (e senza le parentesi). In una chiamata della funzione non è necessario deferenziare ovvero posso scrivere sia
(*pfunz)(12.3,"Ciao");
oppure
pfunz(12.3,"Ciao");

Array puntatori a funzione

In C++ (come in C) è consentito dichiarare anche array di puntatori a funzione, con questa sintassi:

tipoR (*NomeFunz[dim])(tipo1, tipo2, ... , tipoN );

#include 
int quale(char c) { return ( (c=='+') ? 0 : ((c=='-') ? 1 : 2) ); }
int somma(int a, int b) { return a+b; }
int differenza(int a, int b) { return a-b; }
int prodotto(int a, int b) { return a*b; }
int main()
{
    int a, b, r;
    char op;
    int (*funz[3])(int,int)={ somma , differenza , prodotto };
    printf("Calcola: ");
    scanf("%d %c %d",&a,&op,&b);
    r=funz[quale(op)](a,b); // utilizzo il puntatore alla funzione
    //r=(*funz[quale(op)])(a,b); // utilizzo del puntatore dereferenziato
    printf("%d %c %d = %d",a,op,b,r);
    // metto in stop il programma prima della sua chiusura
    fflush(stdin);
    getchar();
    return(0);
}


Parametro puntatori a funzione

Quando invece una funzione dichiara fra i suoi argomenti un puntatore a funzione, allora sono parametrizzate proprio le funzioni e non i loro valori di ritorno. Nelle chiamate é necessario specificare come argomento il nome di una funzione "vera", precedentemente dichiarata, che viene sostituito a quello del puntatore.

Tipo NomeFunzione(param1, param2,...,tipoR (*NomeFunz)(tipo1, tipo2, ... , tipoN ), .. , paramN);

#include <stdio.h>
int min(int a, int b) { return (a > b) ? b : a ; }
int max(int a, int b) { return (a > b) ? a : b ; }
// prototype o forward declaration
int statistica(char *frase, int a, int b, int (*)(int, int));
int main()
{
    int a, b, r;
    printf("Digita 2 numeri a b: ");
    scanf("%d %d",&a,&b);
    statistica("Minimo",a,b,min);
    statistica("Massimo",a,b,max);
    // metto in stop il programma prima della sua chiusura
    fflush(stdin);
    getchar();
    return(0);
}
int statistica(char *frase, int a, int b, int (*stat)(int, int)) { printf("%s: %d\n",frase, (*stat)(a,b)); }
// oppure posso usare il puntatore dereferenziato:
// int statistica(char *frase, int a, int b, int (*stat)(int, int)) { printf("%s: %d\n",frase, stat(a,b)); }

Link utili

http://www.math.unipd.it/~sperduti/CORSO-C%2B%2B/Strutture.htm