L’allocazione della memoria al volo è il modo in cui i programmi C crescono e si restringono durante l’esecuzione. Non conosci le esigenze esatte della tua applicazione finché non viene effettivamente eseguita. La soluzione è il mucchio. Richiedi un blocco. Il sistema operativo lo riserva. Lo usi. Quindi lo restituisci con “free”. Questo ciclo consente ad altri programmi di riciclare la stessa RAM.
Considera questo semplice esempio. È l’interazione più elementare che puoi ottenere con l’heap.
Come funziona realmente Malloc
La funzione malloc non è magica. È un meccanico che controlla l’inventario. Quando lo chiami, accadono tre cose in sequenza.
- Controlla la capacità attuale dell’heap. Controlla il parametro che hai passato, in questo caso
sizeof(int), che è 4 byte. Chiede se esiste abbastanza spazio contiguo. Se l’heap è frammentato o pieno, fallisce. - Se esiste spazio, riserva quel blocco. Il sistema lo contrassegna come “tuo”. Nessun’altra chiamata
mallocpuò toccare questo pezzo. Previene le collisioni accidentali tra le allocazioni. - Restituisce l’indirizzo di quel blocco. Non ti fornisce i dati stessi. Ti dà una variabile puntatore. Quella variabile contiene la posizione. Ora hai un handle per un punto specifico nella RAM.
Nel nostro esempio, “p” riceve quell’indirizzo. La variabile puntatore è solo un contenitore per una posizione di memoria. Il valore intero effettivo risiede in quella posizione.
Perché è necessario verificare la presenza di NULL
Viene visualizzato il controllo “if (p == 0)” immediatamente dopo l’allocazione. Alcuni principianti saltano questo. È un errore fatale.
Controlla sempre il puntatore dopo ogni chiamata a malloc per assicurarti che il puntatore sia valido.
La dimensione dell’heap varia costantemente. Dipende da quali altri processi sono in esecuzione. Dipende da quanta memoria loro hanno catturato. Non vi è alcuna garanzia che una chiamata abbia successo. Se malloc fallisce, restituisce zero. Zero è “NULL”. Se provi a dereferenziare un puntatore nullo, il tuo programma si blocca. Difficile.
Potresti scrivere if (p == NULL) o if (!p). Significano la stessa cosa. Ma devi controllare. Ogni volta.
Cosa succede se dimentichi di liberare memoria?
Se il tuo programma termina e non hai chiamato “free”, il sistema operativo si pulisce. Rilascia lo spazio eseguibile. Cancella lo stack. Recupera la memoria globale. Ricicla inoltre eventuali allocazioni heap in sospeso.
Quindi va bene perdere memoria alla fine di un programma? Tecnicamente sì. Il sistema operativo gestisce la pulizia. Non ci sono conseguenze a lungo termine per la macchina.
Ma è una cattiva forma. E, cosa ancora più importante, le perdite di memoria durante l’esecuzione sono mortali. Se continui ad allocare senza liberare, fai morire di fame il tuo stesso processo. Il mucchio si riempie. Le nuove assegnazioni falliscono. Il programma muore per soffocamento, non per un incidente.
Puntatori e valori a cui puntano
I puntatori possono creare confusione. Sono distinti dai valori a cui fanno riferimento. Ecco come funziona nella pratica.
Primo scenario. Assegni un blocco. Gli indichi due variabili.
Uscita:
10
20
Perché? “p” e “q” sono puntatori diversi. Ma q = p li fa puntare allo stesso blocco di memoria. Modificando il valore tramite “p” lo si cambia in “q”. Cambiandolo tramite “q” lo cambia in “p”. Sono due maniglie sulla stessa porta.
Secondo scenario. Assegni due blocchi separati.
Uscita:
20
Qui, “p” e “q” puntano a posizioni diverse. *q contiene 20. *p = *q copia il valore 20 nella posizione a cui punta p. Il puntatore p stesso non si muove. Il valore all’indirizzo di p cambia.
La distinzione conta. Si cambia l’indirizzo memorizzato nella variabile. L’altro modifica i dati a quell’indirizzo. Confondere i due porta a errori logici che sono più difficili da eseguire il debug rispetto agli errori di sintassi.


Puntatori, memoria e caduta libera
L’output è 20. La riga 6 dice tutto.
Potresti pensare che *p = *q e p = q facciano la stessa cosa. Non lo fanno. Si spostano i dati. L’altro sposta gli indirizzi. Il compilatore ti consente di assegnare *p = *q perché entrambi i puntatori dereferenziati sono interi. È uno scambio diretto di valori. Ma p = q? È diverso. Punta “p” esattamente allo stesso blocco di memoria di “q”. L’indirizzo si sposta. I dati restano fermi.
Se i tipi non corrispondono, il compilatore lo blocca. Un puntatore a un numero intero non può puntare a una stringa di caratteri. I tipi devono essere allineati.
Quattro modi per inizializzare
Una nuova variabile puntatore è una passività finché non viene inizializzata.
int *p; crea un puntatore che non punta a nessun punto specifico. Indica la spazzatura. Dereferenziarlo è un errore. Hai bisogno di una posizione nota. Ci sono quattro modi per arrivarci.
- malloc : alloca un blocco nell’heap. “p” ora contiene un indirizzo valido. Il puntatore viene inizializzato perché punta a una porzione di memoria specifica e riservata.
- Assegnazione :
p = q;seqè valido,pdiventa valido. Eredita l’indirizzo di “q”. Se “q” è spazzatura, anche “p” diventa spazzatura. - Indirizzo esplicito : puoi puntare
pdirettamente su una variabile conosciuta.p = &i;puntapall’interoi. Semplice. Diretto. - Zero/NULL :
p = 0;op = NULL;. Questo imposta l’indirizzo su zero. Non indica un blocco. Punta alla posizione nulla.
Perché usare zero? È una bandiera.
Puoi controllare “if (p == 0)” per vedere se il puntatore è valido. Il sistema lo riconosce. Se provi a dereferenziare un puntatore nullo, il programma si blocca. Difficile.
p = 0; *p = 5;
La seconda riga fallisce. “p” non indica nulla. Non puoi scrivere al nulla. Questo comportamento sarà fondamentale per gli elenchi collegati successivi. È una rete di sicurezza.
Liberare l’heap
Allocare memoria non è sufficiente. Devi restituirlo.
“malloc” occupa spazio. “free” lo restituisce.
“free(p)” fa due cose. Innanzitutto, annulla la prenotazione del blocco. L’heap recupera quella memoria. Può essere riutilizzato. In secondo luogo, “p” viene lasciato non inizializzato. Non diventa magicamente sicuro. È necessario reinizializzare p prima di utilizzarlo nuovamente.
Il blocco è scomparso dal tuo ambito. Il puntatore penzola.
Ecco il modello standard per gestire un singolo numero intero nell’heap:
Questa è principalmente una demo. Ma mostra il ciclo di vita.
“sizeof(int)” restituisce la dimensione in byte. Sulla maggior parte delle macchine, è 4. Potresti codificare malloc(4). Non. sizeof rende il codice portabile. Si legge meglio. Si adatta se cambia l’architettura.
malloc restituisce un puntatore generico. I compilatori spesso avvisano se non lo trasmetti. “(int *)” converte quel blob generico in un puntatore a un numero intero. Corrisponde a “p”.
“free(p)” restituisce il blocco all’heap.
Strutture e precedenza degli operatori
Le strutture si comportano allo stesso modo. Assegni un blocco per l’intera struttura, non solo per i campi.
Guarda (*p).i = 10;.
Perché le parentesi?
Perché . ha la precedenza più alta di *.
Senza parentesi, *p.i significa *(p.i). Tenta di accedere a “i” sul puntatore “p”, quindi dereferenziarlo. Non è quello che vuoi. Vuoi prima dereferenziare p, quindi accedere a i.
Le regole di precedenza sono rigide. “*” è più stretto di “.” in questo contesto. Le parentesi impongono che il dereferenziamento avvenga prima dell’accesso del membro.
La maggior parte delle persone odia digitare (*p).i ripetutamente. È prolisso.
C ha una scorciatoia.
p->i
È esattamente uguale a (*p).i. Ma è più veloce da scrivere. Vedrai -> ovunque nel codice C. È il modo standard per accedere ai membri della struttura tramite un puntatore.
L’heap è una risorsa finita. Assegnare con attenzione. Deallocare in modo coerente. Oppure guarda la tua memoria svanire.


























