Comprendere i puntatori ai puntatori in C

0
8

Sembra un incubo di ricorsione, ma i puntatori a puntatori sono un punto fermo della programmazione dei sistemi. Stai essenzialmente indicando un indirizzo di memoria che contiene l’indirizzo dei tuoi dati effettivi. Questo riferimento indiretto a doppio livello è ciò che gli sviluppatori chiamano handle. Non è solo un trucco di codifica. È un meccanismo necessario affinché i sistemi operativi gestiscano la memoria heap in modo efficiente.

Considera il mucchio come una stanza affollata. A volte il sistema operativo deve spostare le persone in giro per fare spazio. Se tieni un puntatore diretto verso qualcuno, non può muoversi senza interrompere il tuo riferimento. Ma se tieni premuto un puntatore su un elenco di nomi, il sistema operativo può modificare la posizione di qualsiasi persona in quell’elenco senza che tu debba aggiornare il tuo riferimento iniziale. Questo è il potere di una maniglia.

Ecco come questa logica si traduce in codice C grezzo. Dichiari “p” come puntatore a un puntatore. Quindi “q” diventa un puntatore standard. Allochi memoria per p, quindi allochi memoria per ciò a cui punta p. Infine, dereferenzia due volte per assegnare il valore 12.

Windows e macOS si affidano a questa struttura per la compattazione della memoria. La distinzione qui è vitale. Tu, il programmatore, gestisci il puntatore esterno p. Il sistema operativo gestisce il puntatore interno *p. Poiché il sistema operativo controlla *p, può riposizionare il blocco dati effettivo (*p) ovunque nell’heap. Aggiorna semplicemente “ p” per riflettere il nuovo indirizzo. Il tuo codice continua a utilizzare “p” senza intoppi.

Oltre alla gestione della memoria del sistema operativo, questo modello è essenziale per passare i puntatori alle funzioni. Se hai bisogno di una funzione per modificare un puntatore stesso, passi un puntatore a quel puntatore. È l’unico modo per riassegnare il riferimento da un ambito diverso.

Gestione dei puntatori alle strutture

La complessità non si ferma ai semplici numeri interi. Puoi annidare questa logica all’interno delle strutture. Questo è comune quando si gestiscono dati di lunghezza variabile come le stringhe.

Prendi la struttura “Addr”. Contiene array di dimensioni fisse per nomi, città e telefoni. Ma i commenti variano in lunghezza. Quindi comment è definito come un puntatore a char. Quando allochi memoria per la struttura stessa, stai riservando spazio per i puntatori e gli array fissi. Non stai ancora riservando spazio per il testo del commento vero e proprio.

Assegni prima s. Quindi leggi l’input dell’utente nei buffer fissi. Successivamente, leggi il commento in un file temporaneo

Perdi memoria quando ignori il modo in cui si annidano i puntatori.

Prendi un puntatore s che punta a una struttura. Quella struttura contiene un altro indicatore. Il secondo puntatore punta a una stringa effettiva in memoria. Due strati di indiretto. Un’allocazione per la struttura. Un’allocazione per la stringa.

Abbastanza semplice finché non provi a ripulire.

È qui che viaggia la maggior parte degli sviluppatori. Vedi “gratuito(i)”. Pensi di aver finito. Tui hai torto.

Guarda questo frammento di codice:

La variabile s punta alla struttura Addr. All’interno di quella struttura c’è un campo “commento”. “commento” punta a un blocco di memoria heap allocata per i dati della stringa.

Quando chiami free(s), rilasci la memoria per la struttura stessa. La struttura “Addr” è scomparsa. La memoria viene restituita al sistema.

Ma cosa succede a s->comment?

Scompare nell’etere.

Il puntatore ai dati della stringa è stato memorizzato all’interno della struttura appena liberata. Non puoi più accedervi. Non hai chiamato free() sulla stringa. La memoria rimane allocata ma irraggiungibile.

Questa è una perdita di memoria.

Non è un incidente. Non è un messaggio di errore. Il programma funziona bene. Consuma lentamente più RAM finché il sistema non si scambia o si blocca.

Come correggere le perdite di puntatori doppi

Devi prima liberare il puntatore interno. Oppure memorizzalo in una variabile temporanea prima di liberare la struttura esterna.

Ora i dati della stringa vengono rilasciati. Quindi la struttura viene rilasciata. Nessun blocco perso.

Perché ciò accade così spesso

Le persone trattano i puntatori come valori. Dimenticano che i puntatori sono riferimenti a risorse.

Quando una struttura contiene un puntatore, quella struttura non è autonoma. Si basa sulla memoria esterna. Liberare il contenitore non libera il contenuto.

Ciò peggiora con la nidificazione più profonda. Un puntatore a un puntatore a un puntatore. Tre livelli. Sono necessarie tre chiamate free(). Nel giusto ordine.

Se ne dimentichi uno, perdi.

Il problema goes()

L’esempio utilizza gets(). Quella funzione è pericolosa. È stato rimosso dallo standard C11 perché consente overflow del buffer. Ma la logica della memoria rimane la stessa.

Sia che utilizzi gets(), fgets() o scanf(), la strategia di allocazione è il collo di bottiglia per le perdite qui.

Assegni per s.
Assegna per s->comment.

Se liberi solo s, lasci dietro s->comment.

Punti chiave

  • I puntatori doppi richiedono il doppio gratuito.
  • Controlla ogni malloc per un corrispondente free.
  • Se una struttura contiene un puntatore alla memoria dinamica, quella memoria deve essere liberata prima della struttura.
  • free() non libera ricorsivamente i membri.

È facile non vederlo. Il codice viene compilato. Funziona. La fuga di notizie è silenziosa.

Fino a quando non lo è.

Creazione di elenchi collegati

Si può finire con un disastro della memoria se si elimina il contenitore prima dei dati a cui punta. La struttura che contiene il puntatore viene ripulita. Rimane il blocco di corde. Diventa un blocco perduto. Ciò accade quando l’ordine di smaltimento è sbagliato.

Collegamento

Le strutture possono puntare a se stesse. Ciò consente di concatenare record identici. Il risultato è un elenco collegato. È un modo standard per organizzare i dati in C.

Ecco come lo definisci:

typedef struct { char nome[21]; città di carbone[21]; stato carattere[21]; Indirizzo successivo; } Indirizzo;
Indirizzo
primo;

Il campo “successivo” contiene l’indirizzo del record successivo. Si utilizza una singola variabile puntatore per avviare la catena.

Il costo della flessibilità

Il compilatore consente di modificare regole che a prima vista potrebbero sembrare controintuitive. Con sufficiente esperienza, puoi progettare strutture simili a quella mostrata sopra. È una mossa di potere.

Ma non è senza rischi. Stai camminando su una linea sottile tra codice intelligente e spaghetti irraggiungibili.

Perché è importante

Non si tratta solo di sintassi. Riguarda ciò che la lingua ti permette di fare quando ti sposti contro i suoi vincoli.

  • Controllo : ottieni un controllo granulare sul layout della memoria.
  • Interoperabilità : puoi comunicare con le librerie C più facilmente.
  • Prestazioni : a volte, aggirare i controlli di sicurezza consente di risparmiare cicli.

Il problema? Il compilatore non ti salverà da te stesso. Ti consentirà di compilare codice che si blocca in fase di esecuzione.

Come usarlo in sicurezza

Se hai intenzione di farlo, fallo con intento.

  1. Isolalo : non diffondere questo schema in tutta la tua codebase. Conservalo in un modulo piccolo e ben testato.
  2. Documenta tutto : il futuro che ringrazierai ti presenterà. Oppure ti odio. Probabilmente ti odio.
  3. Utilizza astrazioni : racchiudere i puntatori grezzi o i blocchi non sicuri in un’interfaccia pulita. Nascondi il disordine.

Il confronto con la realtà

La maggior parte degli sviluppatori non ne ha bisogno. Utilizzeranno strutture standard. Ne saranno più felici.

Ma quando ti imbatti in un muro in cui la libreria standard non si adatta, sarai felice che il compilatore non ti abbia fermato.

La domanda è se sei pronto a pagare i costi di manutenzione.

È un compromesso. Velocità per la sicurezza. Potenza per la chiarezza.

Scegli tu.

попередня статтяUnix vs Linux: perché la distinzione è importante per gli sviluppatori
наступна статтяCome i sintetizzatori hanno cambiato la musica infrangendo le regole della forma