Per comprendere veramente il motivo per cui i puntatori si comportano in un certo modo, è necessario smettere di pensare in termini di variabili astratte e iniziare a visualizzare l’hardware. È un modello semplice, ma è il fondamento di tutto, dai segfault alle vulnerabilità della sicurezza. Se non hai conoscenze di base su bit, byte e parole, probabilmente dovresti fermarti qui e rispolverare prima quello. Il resto di questa spiegazione presuppone che tu conosca la differenza tra un byte e un bit.
Il tuo computer dispone di memoria o RAM (memoria ad accesso casuale). Potresti averne installati 16, 32 o 64 gigabyte. Questo non è solo spazio di archiviazione; è lo spazio di lavoro. Contiene il codice che la tua CPU sta attualmente eseguendo e i dati che quei programmi stanno manipolando in questo esatto secondo. Pensa alla RAM come a un enorme array lineare di byte.
Ogni singolo byte in quell’array ha un indirizzo. Il primo byte è l’indirizzo 0. Il successivo è 1. Poi 2, 3 e così via. Questi indirizzi funzionano esattamente come gli indici in un array di programmazione standard. Poiché la CPU può passare istantaneamente a qualsiasi indirizzo, si chiama “memoria ad accesso casuale”. Non è necessario leggerlo in sequenza. Può prendere tutto ciò di cui ha bisogno, ogni volta che ne ha bisogno. Quando il compilatore deve memorizzare un tipo di dati più grande, acquisisce semplicemente un blocco di byte contigui. Un numero in virgola mobile standard, ad esempio, occupa solitamente 4 byte contigui.
Diamo un’occhiata ad una dichiarazione globale in C:
Per te, questa è una variabile denominata “f” che contiene un float. Per il computer, questo è un comando per riservare 4 byte specifici nell’array di memoria. Diciamo che il compilatore assegna questi byte alla posizione di memoria 248.440.
Quando scrivi:
Il compilatore non pensa “aggiorna variabile f”. Pensa di “caricare il valore 3.14 nell’indirizzo di memoria 248.440”. L’astrazione è sottile. Sotto il cofano, è tutta una questione di indirizzi e di valori che racchiudono quegli indirizzi.
Questa natura meccanica della memoria porta ad alcuni effetti collaterali sgradevoli. Considera questo frammento di codice C:
Potresti aspettarti che s e t contengano 0:0, 1:1, 2:2, 3:3. E “u” dovrebbe rimanere 0. Invece, l’output assomiglia a questo:
Aspetta. Cosa è successo a “t[0]”? E perché “u” ora è 5?
Il problema è un classico buffer overflow. Osserva attentamente la condizione del ciclo: i<=4. L'array "t" è dichiarato come "t[4]", ovvero ha indici 0, 1, 2 e 3. Quando "i" raggiunge 4, il codice scrive in "t[4]". Tale indice non esiste nello spazio allocato per "t". Scrive un elemento dopo la fine dell'array.
Poiché la memoria è contigua, il computer posiziona s, t e u uno accanto all'altro nell'heap o nello stack.
Quando scrivi oltre la fine di un array, non vai in crash immediatamente. Sovrascrivi tutto ciò che si trova nello slot di memoria proprio accanto ad esso.
In questo caso specifico, scrivere su "t[4]" sovrascrive la posizione di memoria che contiene "u" (che originariamente era 0). Ma perché `u

Scrivi a s[4]. Quell'indice non esiste. Ma al computer non importa. Non controlla se ti stai pestando i piedi. Calcola semplicemente l'indirizzo. E quell'indirizzo finisce su "t[0]".
Pensi di scrivere a s. In realtà stai scrivendo su "t". Il sistema esegue il comando senza lampeggiare. La logica è errata. Il programma corrompe i dati. È silenzioso finché non lo è più.
Adesso prova qualcosa di peggio.
s[1000000] = 5;
Stai scrivendo su una memoria che il tuo programma non possiede. Le conseguenze dipendono interamente dal sistema operativo in esecuzione. Sui sistemi protetti come UNIX, Windows NT o Windows 98, il sistema operativo rileva immediatamente questa violazione. Termina il programma. Sosta dura. Protegge il resto del sistema dal tuo errore.
I sistemi più vecchi come Windows 3.1 o il classico sistema operativo Mac non hanno questo lusso. Non sanno cosa stai facendo. Finisci per sovrascrivere codice o variabili in un'altra applicazione. Il risultato? Un problema tecnico. Uno schianto. O peggio, un guasto a livello di sistema che sembra casuale ma che in realtà è il risultato diretto di un accesso incontrollato alla memoria.
In memoria, variabili come "i", "s", "t" e "u" si trovano una accanto all'altra in indirizzi specifici. Sono vicini. Se scrivi oltre il confine di uno, ti riversi nel successivo. Il computer fa esattamente quello che hai detto. Semplicemente non gli importa che tu abbia avuto buone intenzioni.
Il pericolo di un accesso incontrollato agli array
C e C++ non eseguono il controllo dell'intervallo. Presumono che tu sappia cosa stai facendo. Ciò significa che devi prestare molta attenzione agli intervalli di array. Se leggi o scrivi fuori dai confini, il comportamento è difettoso. Imprevedibile. Pericoloso.
Questa mancanza di sicurezza è il motivo per cui gli errori del puntatore non inizializzato sono così comuni in C. Considera questo codice:
Il compilatore stampa il valore in p e l'indirizzo di i. Inizialmente, "p" contiene spazzatura o zero. L'indirizzo di "i" è solitamente un numero elevato. Ad esempio, potresti vedere:
Dopo p = &i, p contiene l'indirizzo di i. Semplice.
Ora guarda questo:
Questo stampa il valore a cui punta p. Ma "p" non è inizializzato. Punta all'indirizzo 0 o ad un bit di memoria casuale. Il risultato è quasi sempre un errore di segmentazione. Un errore di runtime. Stai tentando di accedere a una memoria che non ti appartiene.
Puntatori sotto una nuova luce
Una volta compresi questi rischi, i suggerimenti cambiano. Non sono solo variabili. Sono indirizzi diretti a posizioni di memoria.
Prendi questo programma:
Ecco cosa sta succedendo:
Come i puntatori memorizzano gli indirizzi di memoria
Quando dichiari una variabile intera come i, occupa 4 byte di spazio nella RAM. Anche un puntatore, chiamiamolo p, occupa 4 byte. Questa dimensione vale per la stragrande maggioranza dei sistemi attualmente in circolazione, dove gli indirizzi di memoria sono lunghi 32 bit. Il settore si sta lentamente spostando verso l'indirizzamento a 64 bit, ma per ora l'impronta a 4 byte è lo standard.
Pensa a i come a una casa. Ha un indirizzo specifico, diciamo 248.440. Il puntatore p non è la casa stessa. È un pezzo di carta con quell'indirizzo scritto sopra. Quando assegni p = &i, stai scrivendo 248.440 su quel foglio.
Questa distinzione è fondamentale per comprendere come funzionano i puntatori in C. Il puntatore non contiene il valore di i. Contiene la posizione in cui vive i. Se dereferenzia p (usando p *), andrai a quell'indirizzo e leggerai il valore. In questo senso, p e i * sono funzionalmente identici. Indicano gli stessi dati.
Visualizzazione dell'indirizzo nel codice
Puoi verificare questo comportamento con una semplice istruzione print.
printf("%d", p);
Questo comando non stampa il valore di i. Stampa il numero scritto sul foglio. L'output è l'indirizzo di memoria effettivo di i.
Perché è importante? Perché ti consente di trasferire posizioni anziché copie di dati. Puoi manipolare direttamente la memoria. Puoi collegare le strutture insieme. È possibile gestire la memoria heap in modo efficiente. Il puntatore è un riferimento. La variabile è il contenuto. Confondere i due porta a bug. Comprendere la differenza porta al controllo.
Alcuni sviluppatori trattano i puntatori come se fossero magici. Non lo sono. Sono solo numeri. Gli indirizzi di memoria sono numeri interi. Trattateli come tali.

















