Menu

Puntatori a struct in C: l'operatore freccia e le struct sull'heap

Come puntare a una struct in C: l'operatore freccia, perché (*p).x ha bisogno delle parentesi, passare le struct per puntatore per modificarle o evitare copie, allocare una struct con malloc e costruire il nodo di una lista concatenata.

Questa pagina include editor eseguibili: modifica, esegui e vedi subito l'output.

Una struct passata a una funzione viene copiata. Va bene per un punto di due int ed è uno spreco per un record da 200 byte, e rende impossibile la modifica, perché la funzione vede sempre e solo la propria copia. Puntare a una struct risolve entrambi i problemi in un colpo solo, ed è anche l'unico modo per costruire strutture che crescono: liste, alberi, grafi.

Prendere un puntatore a una struct

Non serve nulla di speciale. & ti dà l'indirizzo, e il tipo è "puntatore a struct":

Tutte e tre stampano 3. Le ultime due sono la stessa operazione scritta in due modi.

Perché (*ptr).x ha bisogno delle parentesi

Potresti provare a toglierle e scrivere *ptr.x. Compila in qualcosa di molto diverso o, più spesso, non compila affatto, perché l'operatore di membro . ha precedenza più alta della dereferenziazione *:

*ptr.x        // interpretato come *(ptr.x)  -- sbagliato
(*ptr).x      // prima dereferenzia, poi prende il membro -- corretto
ptr->x        // la stessa cosa, detta in breve

ptr.x chiede un membro del puntatore, e un puntatore non ha membri. La lamentela del compilatore ("request for member 'x' in something not a structure or union") è un bug di precedenza travestito.

Dato che la forma con le parentesi è brutta e facile da sbagliare, il C fornisce ->. In pratica non vedrai quasi mai (*p).x nel codice reale; usa p->x e dimentica che la forma lunga esiste, se non come spiegazione di cosa significa la freccia.

Passare una struct per puntatore

Un parametro puntatore dà alla funzione la struct vera del chiamante, quindi le modifiche restano:

Due abitudini da prendere qui:

  • Dichiara const i puntatori in sola lettura. const struct Point *p promette che la funzione non cambierà ciò a cui punta p. Se qualcuno in seguito aggiunge p->x = 0 all'interno, la compilazione fallisce invece di far arrivare il bug in produzione. Documenta anche l'intenzione nel punto di chiamata.
  • Passa un puntatore per le struct grandi anche quando devi solo leggere. Una struct con un buffer per il nome da 64 byte costa 64 byte di copia a ogni chiamata per valore; un puntatore ne costa 8.

Le struct piccole vanno comunque bene per valore: struct Point da 8 byte si copia a buon mercato quanto un puntatore, e la versione per valore non può essere NULL, il che elimina un'intera categoria di errori.

Puntatori dentro array di struct

L'aritmetica dei puntatori funziona sugli array di struct esattamente come sugli array di int: p + 1 avanza di un'intera struct, padding compreso.

Il nome di un array decade a puntatore al primo elemento, quindi nella chiamata staff è già uno struct Employee *. È anche il motivo per cui la funzione ha bisogno di ricevere n separatamente: sizeof dentro la funzione misurerebbe il puntatore, non l'array.

Allocare una struct sull'heap

Le struct sullo stack muoiono alla fine del loro scope. Per crearne una che sopravviva alla funzione che l'ha creata, o per crearne quante ne richiede l'input, alloca con malloc:

In quella piccola funzione quattro dettagli sono tutti fondamentali:

  • sizeof *e, non sizeof(struct Employee). Si legge come "la dimensione di ciò a cui punta e", quindi se il tipo cambia l'allocazione lo segue in automatico. Non c'è modo che i due si disallineino.
  • Controlla NULL. malloc restituisce NULL quando non riesce a soddisfare la richiesta. Scrivere e->id tramite un puntatore nullo è un segmentation fault.
  • Inizializza ogni membro. malloc non azzera la memoria; la struct arriva piena di spazzatura. (calloc(1, sizeof *e) la azzera per te.)
  • Qualcuno deve chiamare free. Restituire un puntatore allocato trasferisce quel compito al chiamante. Dillo in un commento: una regola di proprietà poco chiara è il modo in cui iniziano i memory leak.

Il premio: il nodo di una lista concatenata

Ecco la cosa che le struct non possono fare senza puntatori. Una struct non può contenere se stessa, perché sarebbe infinitamente grande, ma può contenere un puntatore a una del suo stesso tipo, e questo unico trucco costruisce ogni struttura dati collegata in C.

Nota che struct Node *next; fa riferimento a struct Node mentre struct Node è ancora in fase di dichiarazione. È lecito proprio perché un puntatore ha una dimensione nota indipendentemente da ciò a cui punta: il compilatore non ha ancora bisogno della definizione completa. È anche l'unico punto in cui devi usare il tag struct Node anche se hai scritto un typedef: il nome del typedef non esiste ancora dentro la sua stessa definizione.

Il ciclo di free_list salva head->next prima di chiamare free(head). Leggere head->next dopo il free è un use-after-free: la memoria non è più tua, e il valore che leggi è qualunque cosa ci abbia messo l'allocatore.

Errori comuni

  • p->x su un puntatore non inizializzato o NULL. La causa più comune di crash nel codice pieno di struct. Inizializza i puntatori a NULL e controlla prima di dereferenziare.
  • Restituire un puntatore a una struct locale. struct Point *bad(void) { struct Point p = {1,2}; return &p; } restituisce l'indirizzo di memoria che smette di esistere nel momento in cui la funzione restituisce. Restituisci la struct per valore, oppure allocala.
  • Liberare due volte, o dimenticare di liberare. Ogni malloc va in coppia con esattamente un free. Dopo aver liberato, imposta il puntatore a NULL, così un uso accidentale successivo va in crash in modo evidente invece di corrompere la memoria in silenzio.
  • Copiare una struct che contiene un puntatore. L'assegnazione di struct copia il valore del puntatore, non ciò a cui punta. Ora entrambe le struct fanno riferimento allo stesso buffer, e quella che lo libera per prima lascia l'altra con un puntatore pendente.

Domande frequenti

Cosa fa l'operatore -> in C?

p->x accede al membro x tramite il puntatore p. È esattamente equivalente a (*p).x: dereferenzia il puntatore, poi prende il membro. La freccia esiste perché i puntatori a struct sono così comuni che la forma lunga appesantisce il codice.

Perché *p.x non funziona con un puntatore a struct?

Perché . lega più forte di *. *p.x viene interpretato come *(p.x): prende il membro x di p (che è un puntatore, non una struct) e dereferenzia quello. Ti serve (*p).x, oppure scrivi semplicemente p->x.

Meglio passare una struct per valore o per puntatore in C?

Passala per puntatore quando la funzione deve modificare la struct del chiamante, o quando la struct è grande e copiarla sarebbe uno spreco. Passa per valore le struct piccole in sola lettura, dove la copia costa poco e il codice è più semplice. Dichiara i parametri puntatore in sola lettura come const struct T *, così il compilatore fa rispettare la promessa.

Come si alloca una struct con malloc in C?

struct Node *n = malloc(sizeof *n);: la forma sizeof *n chiede la dimensione di ciò a cui punta n, quindi resta corretta anche se in seguito cambi il tipo. Confronta il risultato con NULL, inizializza ogni membro e chiama free(n) quando hai finito.

Illustrazione dei linguaggi di programmazione di Coddy

Impara a programmare con Coddy

INIZIA