Vai al contenuto
Fondamenti di Informatica

Puntatori e memoria dinamica in C

Indirizzi, puntatori e dereferenziazione, passaggio per riferimento, aritmetica dei puntatori e array, malloc e free, errori classici, memoria nei microcontrollori, puntatori a funzione.

I puntatori sono la caratteristica più potente del C e quella che spaventa di più chi lo impara. In realtà l’idea è semplice: un puntatore è una variabile che contiene un indirizzo di memoria. Con i puntatori una funzione può modificare le variabili del chiamante, si scorrono gli array, si alloca memoria durante l’esecuzione e si accede direttamente ai registri dell’hardware. In questa lezione li vediamo passo per passo, insieme alla memoria dinamica e agli errori da evitare.

Indirizzi e puntatori

Ogni variabile occupa una o più celle di memoria, e l’operatore & ne dà l’indirizzo. Un puntatore si dichiara con l’asterisco e contiene un indirizzo; l’operatore * applicato al puntatore dà accesso alla variabile puntata (dereferenziazione).

int x = 42;
int *p = &x;          /* p contiene l'indirizzo di x: p "punta a" x */

printf("%d\n", *p);   /* 42: il valore della variabile puntata */
*p = 7;               /* modifica x attraverso il puntatore */
printf("%d\n", x);    /* 7 */

Il tipo del puntatore (int *, double *, char *) dice che tipo di dato c’è all’indirizzo, e quindi quanti byte leggere. Il puntatore nullo NULL non punta a niente: è il valore da usare per dire “nessun oggetto”, e dereferenziarlo fa terminare il programma.

Passaggio per riferimento

Per permettere a una funzione di modificare una variabile del chiamante, le si passa il suo indirizzo:

void scambia(int *a, int *b)
{
    int t = *a;
    *a = *b;
    *b = t;
}

int main(void)
{
    int x = 3, y = 8;
    scambia(&x, &y);          /* ora x vale 8 e y vale 3 */
    return 0;
}

Così una funzione può anche restituire più risultati, per esempio modulo e fase di un numero complesso, ed è il motivo per cui scanf vuole &x: deve scrivere nella variabile del chiamante.

Puntatori e array

Il nome di un array, in quasi tutte le espressioni, si converte nell’indirizzo del suo primo elemento. E sui puntatori si può fare aritmetica: p + 1 non è l’indirizzo successivo in byte, ma quello dell’elemento successivo.

Un array di interi in memoria con gli indirizzi e un puntatore che punta al terzo elemento
Un array di cinque interi da 4 byte a partire dall’indirizzo 0x1000. Il puntatore p contiene 0x1008, l’indirizzo di a[2]: quindi *p vale 33, e p + 1 punta all’elemento successivo, all’indirizzo 0x100C.

Valgono le equivalenze a[i] == *(a + i) e &a[i] == a + i. Si può scorrere un array con un puntatore:

double media(const double *v, int n)
{
    double s = 0.0;
    for (const double *p = v; p < v + n; p++) {
        s += *p;
    }
    return s / n;
}

La memoria dinamica

Gli array hanno una dimensione fissa, decisa quando si scrive il programma. Se la dimensione si conosce solo durante l’esecuzione (quanti campioni leggere da un file, quanti nodi ha un circuito) si chiede memoria al sistema con malloc, nella zona chiamata heap, e la si restituisce con free quando non serve più.

#include <stdlib.h>

int n = leggi_numero_campioni();
double *campioni = malloc(n * sizeof *campioni);
if (campioni == NULL) {               /* malloc può fallire: va sempre controllato */
    fprintf(stderr, "Memoria insufficiente\n");
    return 1;
}
/* ... si usa campioni[0] ... campioni[n - 1] come un array ... */
free(campioni);
campioni = NULL;                      /* evita di riusarlo per errore */

Gli errori classici

  • Puntatore non inizializzato: int *p; *p = 5; scrive in un indirizzo casuale.
  • Dereferenziare NULL: per esempio non controllare il risultato di malloc.
  • Memory leak: dimenticare free. In un programma che gira per mesi, come il firmware di un router, la memoria si esaurisce un po’ alla volta.
  • Use after free: usare la memoria dopo averla liberata. Il suo contenuto può essere stato riassegnato ad altro.
  • Doppio free: liberare due volte la stessa zona corrompe le strutture interne dell’allocatore.
  • Puntatore a una variabile locale restituito da una funzione: quando la funzione termina, la variabile non esiste più.

Questi errori sono responsabili di una grande parte delle vulnerabilità del software scritto in C e C++: è per questo che linguaggi più recenti come Rust controllano l’uso della memoria già durante la compilazione. Strumenti come valgrind e AddressSanitizer li trovano durante i test.

Nei microcontrollori l’allocazione dinamica si evita quasi sempre: con pochi kilobyte di RAM, allocazioni e liberazioni ripetute frammentano la memoria fino a far fallire malloc dopo ore o giorni di funzionamento. Le linee guida per il software critico, come MISRA C usata nell’automotive, la vietano dopo l’inizializzazione: si usano array statici dimensionati per il caso peggiore.

Puntatori a funzione

Un puntatore può contenere anche l’indirizzo di una funzione, che poi si chiama attraverso di esso. Si usa per scegliere un comportamento durante l’esecuzione: la funzione qsort della libreria standard riceve il puntatore alla funzione che confronta due elementi, e le tabelle dei vettori di interrupt dei microcontrollori sono array di puntatori alle funzioni da eseguire quando arriva un’interruzione.

Subscribe
Notify of
guest
0 Comments
Oldest
Newest Most Voted
0
Would love your thoughts, please comment.x
()
x