C/Vettori e puntatori/Puntatori

Indice del libro

I puntatori, in C, sono uno strumento molto potente e utile, ma sono anche frequente causa di errori insidiosi.

Un puntatore è una variabile che contiene l'indirizzo di memoria di un'altra variabile (il puntatore x punta alla variabile y).

Sintassi modifica

Dichiarazione modifica

Un puntatore si dichiara nel seguente modo:

 int *a;

Si noti l'asterisco prima del nome del puntatore.

 int *a, b;

Il precedente codice non genera due puntatori, ma uno solo, a. La variabile b è un intero, perché l'operatore * vale soltanto per la prima variabile. Qualora si desideri dichiarare più variabili di tipo puntatore, l'asterisco va ripetuto per ciascuna di esse:

 int *a, *b;   // sia a che b sono puntatori a variabile intera

È possibile creare array di puntatori:

 int *a[5];

crea un array di cinque puntatori ad intero. Per maggiori informazioni vedere la sezione sugli array.

Assegnazione modifica

Prima di utilizzare un puntatore, bisogna assegnarlo ad un indirizzo di memoria, ovvero fare in modo che esso punti ad una variabile.

int main (void) 
{
    int *a, b;
 
    b = 10;
    a = &b;
}

Questo codice fa puntare il puntatore a alla variabile b. È importante notare che a non contiene il valore 10, ma l'indirizzo di memoria della variabile b, ottenuto tramite l'operatore &.

È anche possibile fare in modo che un puntatore punti alla stessa variabile puntata da un altro puntatore:

 int *a, *c, b;
 
 b = 10;
 a = &b;
 c = a; // c punta a b

Accesso modifica

 
Il puntatore p punta alla variabile a.

Dopo aver dichiarato e assegnato un puntatore ad una variabile, è possibile usarlo.

int main (void)
{
    int *a, *b, c = 10;
 
    b = a = &c;
    printf("La variabile c, puntata da a, vale %d, mentre la" 
        "stessa variabile puntata da b vale %d.", *a, *b);
}

Questo codice stamperà due volte il numero 10.

Aritmetica dei puntatori modifica

È possibile addizionare o sottrarre ad un puntatore un valore intero. Questo farà in modo che il puntatore punti alla cella di memoria immediatamente successiva.

Se un intero occupa 2 byte, allora se si somma 1 al puntatore che contiene l'indirizzo 100, questo non conterrà 101, ma 102. Questo appunto perché si assume che un intero occupi 2 byte.

Allo stesso modo, se si sottrae 1, il puntatore conterrà il valore 98.

Inoltre, è possibile calcolare la differenza tra due puntatori per capire quante celle di memoria ci sono fra i due. È possibile anche confrontarli per capire se puntano allo stesso indirizzo o meno.

 if (p1 == p2) printf("p1 e p2 puntano allo stesso indirizzo di memoria.");
 else printf("p1 e p2 puntano a due differenti indirizzi di memoria.");

L'aritmetica dei puntatori permette anche l'interscambiabilità tra puntatori e vettori.

Funzioni e puntatori modifica

Se ad una funzione viene passato un puntatore (che può essere l'indirizzo di una variabile tramite l'operatore di indirizzo "&", oppure un puntatore a quella variabile), essa sarà in grado di modificare il valore puntato dal puntatore. tale azione e' detta "passaggio di variabile per riferimento". Per esempio:

void doubleof(int *x); //prototipo
 int main (void)
 {
     int a=2;
     doubleof(&a);
     printf("%d", a);
     return 0;
 }
 
 void doubleof(int *x)
 {
      *x = (* x) * 2;
 }

Questa piccola applicazione stamperà 4. Ecco come:

  1. int a=2; Questa istruzione dichiara una variabile intera a e la inizializza con il valore 2.
  2. doubleof(&a); Chiama la funzione doubleof, passandole l'indirizzo della variabile a.
  3. Dentro doubleof viene eseguita l'istruzione * x = (* x) * 2;, che assegna alla variabile puntata da x (a) il valore della stessa variabile puntata da x moltiplicato per 2.
  4. printf("%d", a); Stampa il valore ottenuto.

se si fosse passata alla funzione la semplice variabile, essa avrebbe restituito 2. questo e' un caso di "passaggio per valore". la funzione creerà una copia della variabile e al termine la cancellerà. NB la variabile passata dal main resterà INALTERATA.

 void doubleof (int); //prototipo
 int main (void)
 {
  int a=2;
  doubleof(a);
  printf("%d", a);
  return 0;
 }
 
 void doubleof(int x)
 {
  x = x*2;
 }

RIASSUNTO:

1) passaggio PER VALORE:

prototipo: void doubleof(int);  per richiamarla: doubleof(a); 
  
viene creata una variabile temporanea. la variabile originale resta INALTERATA.

2) passaggio PER RIFERIMENTO:

prototipo: void doubleof(int*);  per richiamarla: doubleof(&a); oppure doubleof(aPtr); (dopo aver eseguito aPtr=&a)
  
viene passato l'indirizzo e tramite esso la variabile originale sarà MODIFICATA.

Passare un array ad una funzione modifica

Primo metodo modifica

Il nome di un array, generalmente, decade in un puntatore al suo primo elemento. Questo avviene sempre, tranne quando si usa il nome dell'array come argomento di sizeof, quando si usa l'operatore & ("indirizzo di"), o durante un'inizializzazione con una stringa letterale. Tale puntatore può essere passato ad una funzione. In questo modo si può passare un intero array alla funzione.

Per esempio:

 void doubleof(int *ilmioarray);
 
 int main(void)
 {
     int i, arr[5]={1,2,3,4,5};
      
     doubleof(arr);
     for (i=0; i<5; i++)
          printf("arr[%d] = %d.\n", i, arr[i]);
     return 0;
 }
 
 void doubleof(int *ilmioarray)
 {
      int j;
      for(j=0; j<5; j++)
          ilmioarray[j] = ilmioarray[j]*2;
 }

restituisce:

arr[0] = 2.
arr[1] = 4.
arr[2] = 6.
arr[3] = 8.
arr[4] = 10.

Secondo metodo modifica

In questo metodo cambia solo la sintassi della funzione, ma il meccanismo è identico. Il vantaggio di questo meccanismo è che rende palese il fatto che ad una funzione verrà passato un array, ma se alla funzione fosse passato un puntatore non produrrebbe errori in fase di compilazione, ma si potrebbe comportare in modo insolito.

 void doubleof(int ilmioarray[]);
 
 int main(void)
 {
     int i, arr[5]={1,2,3,4,5};
     
     doubleof(arr);
     for (i=0; i<5; i++)
          printf("arr[%d] = %d.\n", i, arr[i]);
     return 0;
 }
 
 void doubleof(int ilmioarray[])
 {
     int j;

     for(j=0; j<5; j++)
         ilmioarray[j]=ilmioarray[j]*2;
 }

Passaggio di matrici a funzione modifica

Ricapitolando una matrice in C altro non è che un vettore di vettore quindi viene naturale la dichiarazione:

tipo Matrice[righe][colonne];

con questa sintassi si dichiara un vettore che contiene un numero pari a colonne di vettori che contengono un numero pari a righe di elementi di tipo tipo. Colonne e righe possono essere delle costanti intere o delle variabili intere (in tal caso si hanno array dinamici caratteristica del C99).

Per poter passare una matrice a una funzione è necessario un puntatore alla matrice che definisca il numero di colonne della matrice quindi il prototipo di una funzione che fa questo è :

tipo_restituito funzione (elenco tipi altri argomenti, tipo [][colonne]);

se il numero di colonne è variabile si usa questa sintassi

tipo_restituito funzione (elenco tipi altri argomenti, tipo [][*]);

Una volta definita la funzione per passarle la matrice basta passarle il nome della variabile matrice che decade in un puntatore alla matrice stessa.

Stesse considerazioni valgono per vettori n-dimensionali ossia del tipo:

tipo v[i1][i2]...[iN];

dove gli indici da i1 a iN possono essere costanti o variabili (standard C99), per passare a una funzione il vettore è necessario definire un puntatore a v dove vengano definite le ultime n-1 dimensioni del vettore ossia il prototipo della funzione è:

tipo_restituito fn (elenco tipi altri argomenti, tipo [][i2]...[iN]);

se le ultime n-1 dimensioni sono variabili:

tipo_restituito fn (elenco tipi altri argomenti, tipo [][*]...[*]);

l'asterisco va solo dove la dimensione è variabile.

Tornando al caso delle matrici segue un esempio commentato sull'uso delle matrici dinamiche e passaggio a funzione.

Esempio passaggio matrici modifica

/* Il seguente codice per funzionare correttamente necessita di un compilatore che supporti
   lo standard C99. */

#include <stdio.h>
void leggiMtrx(int, int, int[][*]);          // il [*] indica che il numero di colonne è 
void stampaMtrx(int, int, int[][*]);         // una variabile.

int main(void){
    int Ri,Co;                               // parametri necessari per definire la matrice
	                                        
    printf("-- acquisizione numero righe e colonne --\n");
    scanf ("%d%d", &Ri, &Co);
    printf("-- Lettura elementi della matrice --\n");
    int M[Ri][Co];                           // Matrice dinamica, è possibile dichiararla
    leggiMtrx(Ri, Co, M);                    // dopo aver letto Ri(ghe) e Co(lonne).
    printf("-- Stampa della matrice --\n");
    stampaMtrx(Ri, Co, M);
}

void stampaMtrx(int Ri,int Co, int M[][Co]){ // qui viene specificato che M è un puntatore
     int i,j;                                // a una matrice di Co colonne dove Co è una 
	                                     // variabile.
     for (i=0;i<Ri;i++){
	for (j=0;j<Co;j++)
	   printf ("%d ",M[i][j]);
	printf("\n");                        // ritorno a capo per distinguere le righe del-
     }                                       // la matrice.
}
void leggiMtrx(int Ri, int Co, int M[][Co]){ // lettura degli elementi della matrice.
     int i,j;
    
     for (i=0;i<Ri;i++)
	for (j=0;j<Co;j++){
	    printf ("M[%d][%d] = ",i,j);
        scanf("%d",&M[i][j]);
	}
}

Puntatori a funzioni modifica

È possibile far puntare un puntatore ad una funzione, in modo che essa possa essere chiamata attraverso il puntatore. Anche le funzioni, come le variabili, hanno un indirizzo di memoria.

Il seguente esempio definisce, assegna e utilizza un puntatore a funzione:

 #include <stdio.h>

 int doubleof(int x)
 {
  return x*2;
 }

 int main(void)
 {
  int (*funz)(int);
  funz=doubleof;
  printf("Il doppio di 2 è %d", funz(2));
  return 0;
 }

Si noti che il tipo del puntatore "funz" è uguale al tipo dalla funzione "doubleof". Il tipo di una funzione è costituito dalla sequenza dei tipi dei parametri e dal tipo di ritorno. Quindi per assegnare un indirizzo di una funzione a un puntatore a funzione è necessario che la funzione e il puntatore abbiano lo stesso numero di parametri, che i parametri corrispondenti abbiano lo stesso tipo, e il tipo di ritorno sia lo stesso.

Da notare inoltre che in questo caso non è stato usato l'operatore di referenziazione & come negli esempi precedenti, infatti, i nomi delle funzioni sono già dei puntatori.

Puntatori a puntatori modifica

È possibile definire anche puntatori ad altri puntatori che potrebbero a loro volta puntare a puntatori ecc.

#include <stdio.h>
int main (void)
{
    int *p, **pp, a = 5;
    
    p = &a;                    // p punta a
    pp = &p;                   // pp punta a p
    printf ("**p = %d", **pp); // stampa il contenuto di a   
}

il risultato è:

**p = 5