Tutorial: i puntatori

I puntatori sono una delle cose più temute dai programmatori nuovi dei linguaggi C e C++, ma prima o poi bisogna averci a che fare. Ho pensato di creare questo piccolo tutorial. Vi garantisco che se capirete bene tutto fino in fondo e sarete capaci di riprodurre quello che vi illustrerò, non avrete alcun problema in futuro con i puntatori.

Cos’è un puntatore?
Un puntatore non è altro che una particolare variabile che contiene un indirizzo di memoria, spesso, ma non necessariamente, quello di un’altra variabile.
Cosa ce ne facciamo di una roba del genere? Posso garantirvi che è molto più utile di quanto sembri, tanto è vero che in alcuni linguaggi usate implicitamente i puntatori, come il Java, in cui ogni variabile corrispondente a un oggetto è in realtà un puntatore! In C e C++ la gestione dei puntatori è esplicita e richiede molta attenzione perché facilita la creazione di bug fastidiosi e difficili da individuare.
Gli esempi qui riportati sono tutti in C, ma vanno bene anche per C++.
Il prerequisito per capire questo tutorial è di conoscere i rudimenti del C o del C++, possibilmente con un po’ di pratica. Se siete tipi svegli, dovreste capirlo anche se siete capaci di programmare con un altro linguaggio.

Il primo esempio:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
#include <stdio.h>
 
void scambia(int *a, int *b){
  int tmp;
 
  tmp = *a;
  *a = *b;
  *b = tmp;
}
 
int main(){
  int a = 9, b = 5; /* Variabili */
  int *pa, *pb; /* Puntatori */
  pa = &a; /* Assegnamo a pa l'indirizzo di a */
  pb = &b; /* Assegnamo a pb l'indirizzo di b */
 
  /* Scriviamo i valori di a e b attraverso i puntatori*/
  printf("a: %d, b: %d\n", *pa, *pb); /* Scrive "a: 9, b: 5" */
  scambia(pa, pb);
  printf("a: %d, b: %d\n", *pa, *pb); /* Scrive "a: 5, b: 9" */
 
  pb = pa; /* Ora sia pa che pb puntano ad a */
  printf("*pa: %d, *pb: %d\n", *pa, *pb); /* Scrive "*pa: 5, *pb: 5" */
 
  *pb = 12;
  printf("*pa: %d, a: %d, b: %d\n", *pa, a, b); /* Scrive "*pa: 12, a: 12, b: 9". Se hai capito perché, hai capito i puntatori! */
 
  return 0;
}

Questo è un esempio di base sui puntatori.
Tre sono le cose fondamentali da sapere:

  1. Se scriviamo
    int *p;

    stiamo dichiarando un puntatore a una variabile di tipo intero. Ovviamente possiamo dichiarare puntatori a qualsiasi tipo di dato.
  2. Scrivendo
    p = &a;

    assegnamo al puntatore p l’indirizzo della variabile a
  3. Se scriviamo
    *p = 12;

    stiamo assegnando 12 alla variabile puntata da p. Se consideriamo l’assegnamento che abbiamo fatto nel punto 2, ora la variabile a varrà 12.

Nel nostro esempio, dichiariamo due variabili e ne scambiamo i valori attraverso una procedura, poi ci giochiamo un po’, tanto per fissare i concetti!.
Perché alla procedura di scambio abbiamo passato dei puntatori invece di passare direttamente le variabili? Perché quando passiamo una variabile a una qualsiasi funzione in C, in realtà passiamo una copia del suo valore. Se ci interessa modificare la variabile all’interno della procedura, dobbiamo passare come parametro un puntatore alla variabile. Questo si fa a volte anche per risparmiare spazio in memoria e migliorare le prestazioni: se una procedura deve lavorare con una struttura molto grande, il passaggio di dati per copia può essere molto costoso, mentre passare un puntatore è come passare un semplice intero!
Chiaro fin qui?

Allocazione dinamica della memoria

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
#include <stdio.h>
#include <malloc.h>
 
struct coppia{
  int val1;
  int val2;
};
 
int main(){
  /* Allocazione statica di una struttura coppia */
  struct coppia c1;
  /* Modifica dei campi */
  c1.val1 = 1;
  c1.val2 = 10;
 
  /* Dichiariamo un puntatore a struttura coppia e assegnamo l'indirizzo di c1 */
  struct coppia *pc = &c1;
  /* Per riferirci a un campo della struttura puntata possiamo usare l'operatore -> sul puntatore, che è come scrivere (*puntatore).campo! */
  printf("val1: %d, val2: %d\n", pc->val1, (*pc).val2); /* Scrive "val1: 1, val2: 10" */
 
  /* Allocazione dinamica di una struttura */
  pc = (struct coppia*)malloc(sizeof(struct coppia));
  pc->val1 = 13;
  pc->val2 = 23;
  printf("val1: %d, val2: %d\n", pc->val1, (*pc).val2); /* Scrive "val1: 13, val2: 23" */
 
  /* Deallocazione della struttura allocata dinamicamente. */
  free(pc);
}

Ecco qui un altro uso dei puntatori. In C e C++ abbiamo due modi per allocare qualsiasi variabile: allocazione statica o dinamica. Allocare una variabile vuol dire semplicemente riservare la memoria per memorizzarla. Con l’allocazione statica la memoria è riservata quando si dichiara la variabile e resta lì finché non si esce dal programma. Con l’allocazione dinamica la memoria può essere riservata in qualsiasi momento durante l’esecuzione (a run time) ed eventualmente liberata affinché lo spazio sia disponibile per altre allocazioni. Entrambi i modi servono, a seconda dei vari usi.
Nell’esempio abbiamo dichiarato una struttura che contiene una coppia di valori e l’abbiamo allocata nei due modi possibili. Per l’allocazione dinamica ci serve obbligatoriamente un puntatore per avere modo di riferirci alla memoria appena riservata. L’indirizzo di memoria da associare al puntatore ci viene restituito da malloc, funzione con cui possiamo allocare dinamicamente quantità arbitrarie di memoria. Nell’esempio appena visto non era necessario liberare la memoria con la funzione free, dato che quando il programma termina, il sistema operativo rilascia tutta la memoria da lui occupata.

Mettiamo tutto in pratica, complicandoci un po’ la vita
L’esempio che vedrete ora è più complesso, ma non troppo.
Link al codice sorgente: http://www.ilbytecidio.it/altri_file/pila.c
Con questo esempio impareremo a costruire una pila (o stack). Una pila è una struttura dati gestita con politica LIFO (Last In First Out). Questo significa che possiamo inserire e togliere dati solo in testa, come se i nostri valori fossero una pila di piatti: non li possiamo sfilare dal centro! Nel nostro caso si tratta di una pila di caratteri.
Partiamo dall’inizio. Per creare la nostra pila usiamo una struttura “nodo” che contiene un carattere e un puntatore all’elemento successivo della pila. In parole povere, la implementeremo usando una lista concatenata.
Ci sono modi più semplici, ma noi vorremmo gestire la pila in modo astratto, cioè nascondendo all’utente più dettagli possibile circa la sua implementazione. Perché questo sia possibile, ci serve una serie di funzioni che agiscano direttamente sulla pila.
Sulla riga 10 vediamo che pila e nodo sono due tipi praticamente uguali, che indicano la struttura nodo. Avremmo potuto scrivere typedef *nodo pila per nascondere qualche dettaglio in più, ma volevo che l’uso dei puntatori fosse più esplicito possibile, per chiarire l’esempio.
Nel main, possiamo vedere che la pila è dichiarata come puntatore. Per la precisione sarà un puntatore al primo nodo.
Vediamo le funzioni che usiamo per la gestione. La prima (riga 13) serve a inizializzare la pila. Questo viene fatto mettendo a NULL il puntatore alla pila. NULL è un particolare valore che indica che il puntatore non sta puntando ad alcun indirizzo. Quando il puntatore alla pila è NULL, significa che la pila è vuota. Notate qualcosa di strano? Forse che il parametro è un puntatore a un puntatore! Se avete prestato attenzione al paragrafo precedente, dovrebbe essere chiaro che se vogliamo modificare una qualsiasi variabile all’interno di una procedura dobbiamo passarle un puntatore. Questo vale anche se la nostra variabile è a sua volta un puntatore!
La seconda funzione è la push, con cui inseriamo i valori in testa alla pila. Per prima cosa si alloca un nuovo nodo, quindi gli si assegna il valore e si fa puntare il suo campo prossimo al nodo che fin’ora era in testa. E il puntatore alla testa della pila punterà al nuovo nodo!
La terza funzione è la pop, con cui togliamo il valore in testa alla pila. L’aspetto saliente è che salviamo temporaneamente l’indirizzo del nodo che stiamo eliminando per poter liberare la memoria da esso occupata, dopo aver aggiustato il puntatore della pila.
Le ultime due funzioni non necessitano di un puntatore a puntatore perché non fanno modifiche. La penultima restituisce il valore in testa alla pila e l’ultima ci permette di controllare se la pila è vuota.
Il resto è decisamente banale: si chiede all’utente di inserire una stringa, i caratteri digitati sono inseriti uno ad uno nella pila e poi questa è svuotata, scrivendo i caratteri man mano che li si estrae. Come possiamo vedere, alle funzioni a cui serve il puntatore al puntatore della pila, passiamo l’indirizzo di mia_pila che a sua volta è il puntatore alla pila.
Capito questo esempio siete a posto per sempre coi puntatori. Come esercizio di comprensione vi lascio il compito di creare un’altra struttura dati astratta: la coda, in cui, al contrario della pila i valori sono inseriti in fondo ed estratti in testa (politica FIFO, First In First Out).

È tutto?
Quasi. Quello che sto per dirvi era definito dalla mia insegnante di programmazione all’università come “il Vangelo”, guai dimenticarlo!
In C e C++ il nome di un array è un puntatore al suo primo elemento.
Quindi queste istruzioni sono perfettamente lecite:

1
2
3
char prova[5] = "Ciao\0";
putchar(prova[2]); /* Stampa "a" */
putchar(*(prova + 2)); /* Stampa "a" */

Come avrete sentito dire, C e C++ sono linguaggi di livello relativamente basso, ciò significa che mancano molte delle astrazioni presenti in altri linguaggi. Da un lato significa avere molta libertà e prestazioni migliori, dall’altro vuol dire avere poco aiuto sia dal compilatore che a run time, quindi per evitare errori dobbiamo acquisire molta padronanza e prestare attenzione a ciò che facciamo. Per esempio, se in Java non è possibile uscire dai confini di un array, in C lo è! Se puntiamo per sbaglio a un inesistente sesto elemento di un array di cinque o facciamo qualsiasi altro errore nell’assegnamento di un puntatore, il compilatore NON ci avviserà e non è nemmeno detto che il programma inizi a funzionare male da subito.. potremmo accorgercene in un momento a caso, giusto per rendere le cose più divertenti!

Ora che vi ho spaventato, è davvero tutto! Buon divertimento!

Questo articolo è stato pubblicato in C, C++ da Alex . Aggiungi il permalink ai segnalibri.

Lascia un commento

Il tuo indirizzo email non sarà pubblicato. I campi obbligatori sono contrassegnati *