Puoi puntare a quasi tutto in C. Il linguaggio ti consente di creare puntatori per tipi primitivi, array, funzioni e tipi definiti dall’utente. Ma i puntatori alle strutture sono il punto in cui le cose si fanno interessanti. Sono estremamente comuni. Se stai scrivendo codice C, probabilmente li incontrerai spesso.
Ecco un esempio di base. Mostra come definire una struttura e quindi creare un puntatore ad essa.
Questo frammento fa molto di più che assegnare semplicemente un indirizzo di memoria. Stabilisce una relazione tra la variabile “ptr” e la struttura “p1”. Il puntatore contiene l’indirizzo della struttura. Ti consente di accedere indirettamente ai membri “x” e “y”. Questo è uno schema fondamentale. Lo vedi negli elenchi collegati, nell’allocazione dinamica della memoria e negli argomenti delle funzioni.
Perché utilizzare i puntatori alle strutture?
Copiare intere strutture è costoso. Se la tua struttura contiene molti dati, passarla per valore a una funzione significa che il compilatore ne esegue una copia completa. Questo fa perdere tempo e memoria. L’uso di un puntatore evita la copia. Invece passi l’indirizzo. La funzione può modificare i dati originali senza sovraccarico.
C’è anche la questione del dimensionamento dinamico. Gli array hanno dimensioni fisse in fase di compilazione. Le strutture non risolvono necessariamente da sole il problema dinamico. Ma quando combini una struttura con malloc, ottieni un’archiviazione flessibile dei dati. È possibile allocare una struttura nell’heap. Un puntatore a tale struttura consente di gestirne manualmente la durata.
Accesso ai membri tramite puntatori
Una volta che hai un puntatore, devi accedere ai dati all’interno. Non è possibile utilizzare l’operatore punto. L’espressione “ptr.x” non verrà compilata. “ptr” è un indirizzo, non la struttura stessa. Devi prima dereferenziarlo.
Ci sono due modi per farlo. Il primo è prolisso.
Le parentesi qui sono obbligatorie. L’operatore punto ha una precedenza maggiore rispetto all’operatore di dereferenziazione. Senza di essi, il compilatore tenta di accedere a ptr.x, che non esiste.
Il secondo modo è più pulito. C fornisce l’operatore freccia ->. Combina il dereferenziamento e l’accesso ai membri in un unico passaggio.
Questa sintassi è standard. È ciò che vedrai nelle codebase reali. Si legge naturalmente come “vai all’oggetto puntato da ptr e ottieni il suo membro x.” È conciso. Riduce il disordine visivo.
Insidie comuni
I puntatori nulli sono un rischio. Se dichiari un puntatore ma non lo inizializzi, punta a spazzatura. L’accesso a ptr->x su un puntatore nullo provoca un errore di segmentazione. È necessario verificare la presenza di null prima di utilizzare il puntatore.
Questo controllo non è facoltativo nel codice robusto. Previene gli arresti anomali.
Un altro problema sono le perdite di memoria. Se allochi memoria con malloc e perdi il puntatore, quella memoria rimane riservata fino al termine del programma. È necessario tenere traccia di ogni allocazione. free() è tua responsabilità. Dimenticalo
I puntatori a strutture in C spesso inciampano gli sviluppatori che hanno dimestichezza con i puntatori di base ma rimangono ingarbugliati nella sintassi. Considera una semplice definizione di record per un profilo utente.
struttura typedef {
carattere nome[21];
città di carbone[21];
stato carattere[3];
} Registra;
typedef Rec *RecPointer;
Qui, “RecPointer” è solo un alias per un puntatore a una struttura “Rec”. Quando dichiari una variabile di questo tipo, hai a che fare con un indirizzo di memoria, non con i dati stessi.
RecPointer r;
La variabile “r” occupa quattro byte su un sistema a 32 bit (o otto su 64 bit). È solo un indicatore. Non contiene il nome, la città o lo stato. Per archiviare i dati effettivi, è necessario allocare memoria nell’heap.
r = (RecPointer)malloc(dimensione(Rec));
Questa chiamata “malloc” riserva 45 byte. Cioè 21 byte per il nome, 21 per la città e 3 per lo stato, più un byte per il riempimento o l’allineamento per soddisfare i limiti della memoria. Ora r punta a un blocco di memoria valido che si comporta esattamente come una struttura Rec.
Accesso ai membri tramite dereferenziazione
Per interagire con i dati, è necessario dereferenziare il puntatore. È qui che si insinuano gli errori di precedenza. È possibile accedere ai membri utilizzando la notazione punto standard, ma è necessario racchiudere il riferimento tra parentesi.
strcpy((r).nome, “Leigh”);
strcpy(( r).città, “Raleigh”);
strcpy((*r).state, “NC”);
Notare la sintassi. “(*r).nome” è corretto. Se scrivi *r.name, la compilazione non riesce. Perché? Perché l’operatore punto ha una precedenza maggiore rispetto all’operatore di dereferenziazione. Il compilatore interpreta *r.name come *(r.name). Poiché “r” è un puntatore, “r.name” ha una sintassi non valida e l’espressione collassa. Le parentesi forzano il dereferenziamento *r ad avvenire per primo, producendo la struttura, e poi l’operatore punto accede al campo name.
È noioso scrivere. Sembra disordinato. Invita agli errori.
La notazione della freccia
C fornisce un modo più pulito per gestire questa situazione. L’operatore freccia -> è lo zucchero sintattico per (*pointer).member. Non è un meccanismo diverso. Non è un nuovo operatore che cambia il modo in cui si accede alla memoria. È semplicemente un modo più breve per scrivere il dereferenziamento e l’accesso dei membri in un unico passaggio.
strcpy(r->nome, “Leigh”);
Questo è identico a strcpy((*r).name, "Leigh"). Salva due personaggi. Elimina la necessità di parentesi annidate. È il modo standard in cui la maggior parte degli sviluppatori C interagisce con i puntatori alla struttura.
Implicazioni sulla gestione della memoria
La chiamata “free(r)” è obbligatoria. La memoria è stata allocata dall’heap. Se non lo rilasci, perde. Il puntatore r stesso, la variabile a quattro byte che contiene l’indirizzo, è locale allo stack frame o all’ambito globale, ma i dati a cui punta risiedono altrove.
Quando usi “r->name”, stai modificando i dati all’indirizzo memorizzato in “r”. Il puntatore “r” rimane invariato. L’indirizzo sì

L’allocazione immediata della memoria per gli array è un elemento fondamentale della programmazione C, ma richiede la comprensione di come i puntatori interagiscono con i blocchi di memoria grezza. Quando hai bisogno di un array di dimensioni fisse che non è noto in fase di compilazione, l’allocazione dello stack standard non sarà sufficiente. Devi raggiungere il mucchio.
Lo snippet di codice riportato di seguito mostra un modello comune:
Qui, malloc riserva spazio per dieci numeri interi. Il cast su “(int )” garantisce che il puntatore corrisponda al tipo previsto, anche se i moderni compilatori C spesso mettono in guardia contro cast espliciti per “void “. Il ciclo quindi inizializza ciascun elemento su zero utilizzando la notazione in pedice. Infine, “free” rilascia nuovamente la memoria al sistema.
Ma questo non è l’unico modo per scriverlo.
Puoi ottenere esattamente lo stesso risultato sostituendo p[i] con l’aritmetica dei puntatori:
Perché è importante? Perché p[i] è solo zucchero sintattico per *(p+i). Il compilatore li tratta in modo identico. Se lavori con sistemi embedded o scrivi cicli stretti in cui ogni ciclo conta, sapere che questi sono intercambiabili ti aiuta a leggere il codice legacy e a scriverne uno tuo senza confusione.
Tuttavia, c’è una trappola sottile.
Se dichiari direttamente un puntatore a un tipo di array, come int (*p)[10], hai a che fare con una bestia completamente diversa. Quel puntatore punta all’intero array, non solo al primo elemento. Incrementandolo si sposta il puntatore della dimensione dell’intero array, non di un singolo numero intero. La maggior parte degli sviluppatori utilizza int * perché è più semplice e flessibile.
Quando utilizzare quale approccio
La scelta tra la notazione in pedice e l’aritmetica esplicita del puntatore spesso dipende dalla leggibilità e dall’intento.
- Utilizza
p[i]quando vuoi enfatizzare l’accesso basato sull’indice. È più chiaro per la maggior parte dei lettori. - Usa
*(p+i)quando stai manipolando la memoria di basso livello o hai bisogno di evitare problemi di decadimento dell’array in espressioni complesse.
Entrambi gli approcci richiedono un’attenta gestione della memoria. Dimentica “gratis” e perderai. Chiamalo troppo presto e ti imbatterai in un comportamento indefinito. E sebbene malloc sia semplice in questo caso, verifica sempre che il valore restituito non sia “NULL” prima di dereferenziare.
“I puntatori agli array sono potenti, ma richiedono disciplina. Un passo falso e stai leggendo spazzatura o, peggio, corrompendo la memoria di un’altra variabile.”
In pratica, la maggior parte degli sviluppatori raramente ha bisogno di scrivere l’aritmetica dei puntatori grezzi per array semplici. Librerie come std::vettore in C++ o astrazioni di livello superiore in altri linguaggi lo gestiscono automaticamente. Ma in C? Sei da solo.
Ed è per questo che è importante comprendere i meccanismi. Non solo per superare interviste o scrivere libri di testo, ma per quando il codice si rompe alle 2 del mattino e devi sapere esattamente cosa sta succedendo nella memoria.

Quando dichiari un puntatore a un array di numeri interi, non stai creando nulla di esotico. È solo un puntatore standard a un “int”. La magia avviene con “malloc”. Allochi un blocco di memoria abbastanza grande per il numero di numeri interi di cui hai bisogno. Il puntatore quindi prende di mira il primo elemento di quel blocco.
A C non importa come accedi. Puoi utilizzare parentesi quadre come p[5] o utilizzare l’aritmetica dei puntatori come *(p + 5). Il compilatore li tratta come identici. Questa flessibilità è il motivo per cui gli array dinamici sono così utili per le stringhe. Non indovinare la dimensione. Allocare spazio di archiviazione esattamente sufficiente per la lunghezza della stringa più il terminatore null.
Matrici di puntatori e matrici di strutture
Perché usare un array di puntatori quando potresti semplicemente usare un array di strutture? Spazio. O meglio, la sua mancanza.
Consideriamo una struttura Rec con tre array di caratteri di 81 byte ciascuno. Ciò significa 243 byte per record. Se dichiari “Rec records[10]”, riserverai immediatamente 2.430 byte in memoria. Tutto. Anche se usi sempre e solo un record.
Una serie di puntatori cambia i calcoli.
L’array a stesso contiene solo 10 puntatori. Su un sistema a 64 bit, ovvero 80 byte. Si tratta di una frazione della memoria richiesta per le strutture complete. La memoria per i record effettivi rimane inutilizzata finché non ne hai bisogno.
È possibile allocare un singolo record su richiesta.
Questo modello risolve i problemi ad uso intensivo di memoria rinviando l’allocazione. Paghi solo per quello che usi. Quando hai finito con il record, chiami “libero”. Il puntatore diventa un riferimento pendente, ma la memoria viene restituita al sistema.
Strutture contenenti puntatori
Le strutture possono contenere puntatori. Ciò consente di combinare dati a dimensione fissa con dati a dimensione variabile nello stesso oggetto.
Prendi una voce della rubrica. Il nome, la città e il numero di telefono potrebbero avere lunghezze massime ragionevoli. Un commento, tuttavia, potrebbe essere qualsiasi cosa, da una singola parola a un romanzo. Non vuoi sprecare spazio assegnando un buffer enorme per ogni voce nel caso in cui una persona scriva un lungo commento.
La stessa struttura “Addr” è piccola

Come i campi dei commenti gestiscono i record vuoti e quelli popolati
Non tutti i record nel database contengono un commento. Quando un campo viene lasciato vuoto, non rimane vuoto. Contiene un puntatore. Nello specifico, un puntatore a 4 byte che non punta a nulla di sostanziale. Il sistema tratta questa assenza come uno stato valido. La documentazione è ancora completa. I metadati sono intatti.
Ma cosa succede quando un utente digita effettivamente qualcosa?
L’allocazione cambia. Il database non riserva un buffer fisso per questi commenti. Non indovina una lunghezza massima e riempie il resto con byte nulli. Ciò sprecherebbe spazio. Invece, il sistema calcola la lunghezza esatta della stringa. Quindi alloca esattamente quel numero di byte.
Questa allocazione dinamica è efficiente. Previene la frammentazione del gonfiore. Una breve nota richiede alcuni byte. Un lungo saggio richiede di più. Il puntatore nel record vuoto punta a un riferimento nullo, mantenendo l’impronta minima. I record con contenuto si estendono per adattarsi ai dati. Niente è sprecato. Niente è forzato.
È questo l’unico modo per memorizzare il testo? No. Ma è un modo intelligente per bilanciare velocità e spazio. Ottieni la reattività delle intestazioni a dimensione fissa con la flessibilità dei dati a lunghezza variabile. È un piccolo dettaglio. Un meccanico di basso livello. Ma i conti tornano quando gestisci milioni di record. Il database respira più facilmente. L’utilizzo del disco rimane ridotto.
E l’utente? Non vedono mai il puntatore. Vedono solo il loro commento. O la mancanza di uno. La complessità è nascosta. Lo spazio di archiviazione è ottimizzato. Il risultato è un sistema che sembra leggero, anche quando i dati crescono.
































