Come i puntatori C vengono effettivamente mappati sulla memoria dell’array

6

C rende gli array e i puntatori inseparabili. Non è possibile scrivere codice efficace senza comprendere la loro relazione. Ci vuole tempo per padroneggiarlo. Il risultato è il controllo immediato sulla memoria.

Considera una configurazione di base. Dichiari due array interi di dimensione dieci.

Compila questo. Fallisce. Non è possibile assegnare un array a un altro utilizzando il segno di uguale. C non supporta la copia in blocco tramite operatori di assegnazione. Per copiare a in b, è necessario scorrere ogni elemento.

Potresti comprimere quel ciclo in una singola riga con un operatore virgola, ma la leggibilità ne risente. L’approccio standard consiste nell’utilizzare memcpy da string.h. Gestisce la copia a livello di byte in modo efficiente.

Il fallimento di b=a rivela una regola fondamentale. In C, le variabili di array non sono array. Sono puntatori fissi. a e b sono identificatori permanenti per l’indirizzo iniziale dei rispettivi blocchi di memoria. a punta a “a[0]”. b punta a “b[0]”. Questi indirizzi sono immutabili. Non è possibile reindirizzare un nome di variabile in una posizione di memoria diversa. Ecco perché l’incarico fallisce. Il compilatore tratta il nome dell’array come un puntatore costante.

Poiché il nome è effettivamente un puntatore, è possibile utilizzare l’aritmetica del puntatore per accedere ai dati. È qui che la sintassi diventa potente.

Qui, p è una variabile puntatore. È possibile assegnare l’indirizzo dell’array a a p. L’istruzione “p=a” copia l’indirizzo. Ora p ha lo stesso valore del nome dell’array a. Dereferenziare p con *p recupera il primo elemento. L’output è zero.

Questa distinzione è importante perché le variabili puntatore possono essere modificate. Puoi fare in modo che p punti a “a[1]” o “a[5]” utilizzando l’aritmetica. Il nome dell’array stesso rimane bloccato all’inizio. Questa flessibilità consente alle funzioni di scorrere la memoria senza copiare strutture di dati di grandi dimensioni. È la spina dorsale delle prestazioni di basso livello in C.

Comprendere questo collegamento previene bug sottili. Spiega perché alcune funzioni accettano puntatori invece della sintassi dell’array. Chiarisce il layout della memoria. Ignoralo e dovrai lottare con errori di segmentazione o sovrascritture involontarie dei dati. Imparalo e vedrai esattamente cosa sta facendo la macchina.

All’inizio la sintassi potrebbe sembrare innaturale. Il doppio significato di parentesi e asterischi confonde i principianti. Ma una volta che il modello scatta, l’astrazione svanisce. Ti rimangono indirizzi e valori. Niente di più.

Vale la pena fare uno sforzo mentale? Il codice diventa più veloce. L’utilizzo della memoria diminuisce. Ottieni l’accesso diretto alle operazioni a livello hardware. La maggior parte dei programmi C fa affidamento

È possibile assegnare una variabile di array direttamente a un puntatore perché, in fondo, sono essenzialmente la stessa cosa.

p=a; funziona non per magia, ma perché a decade in un puntatore. Nello specifico, punta all’indirizzo di memoria del primo elemento dell’array (indice 0). Dato che l’elemento è un numero intero, a agisce come puntatore a un singolo int. Dichiarare p come puntatore intero e impostarlo uguale a a significa semplicemente lasciare che due variabili condividano lo stesso indirizzo. Potresti invece scrivere p=&a[0];. Il risultato è identico.

Una volta che p è collegato all’inizio di a, ottieni mobilità. I nomi di array come a sono costanti; sono indirizzi fissi che non possono cambiare. Puntatori come p sono diversi. Puoi spostarli usando l’aritmetica del puntatore.

È qui che C diventa intelligente.

Quando scrivi p++;, il compilatore non aggiunge solo un byte all’indirizzo. Sa che p punta a un numero intero. Aggiunge invece sizeof(int) byte, atterrando esattamente sull’elemento successivo. Se p puntasse ad una struttura lunga 100 byte, p++ salterebbe in avanti di 100 byte. C gestisce i calcoli. Basta spostare il puntatore.

Copia di array senza indici

Puoi copiare un array in un altro usando questi puntatori. Invece di un ciclo standard con indici:

Puoi usare i puntatori:

È prolisso. Puoi comprimerlo.

Oppure comprimerlo in un’unica riga.

La precedenza dell’operatore qui sta svolgendo lavori pesanti. * ha una precedenza maggiore rispetto a ++, ma il post-incremento avviene dopo che il valore è stato recuperato. Quindi *p++ recupera il valore corrente, quindi sposta il puntatore. *q++ fa lo stesso per la destinazione. L’incarico avviene nel mezzo. È compatto. È veloce. È anche facile fraintendere se non presti attenzione.

La zona pericolosa

Cosa succede se spingi p o q oltre la fine dell’array?

C non ti ferma.

Il compilatore presuppone che tu sappia cosa stai facendo. Continuerà ad incrementare i puntatori, copiando i dati nella memoria che non appartiene all’array. Potresti sovrascrivere altre variabili. Potresti corrompere lo stack. Potresti mandare in crash il programma in un secondo momento, in un punto non correlato al bug.

Questo è il motivo per cui il controllo dei limiti è manuale in C. Il linguaggio si fida di te. Quando esci dalla linea, ne paghi il prezzo.

Passaggio di array alle funzioni

Puoi passare gli array alle funzioni in due modi. Sembrano diversi ma si comportano in modo simile.

Considera una funzione che stampa un array:

O la versione del puntatore:

Il parametro “nia” (numero nell’array) è obbligatorio. Gli array in C non portano con sé la loro dimensione. La funzione deve sapere quando fermarsi.

In entrambi i casi, stai passando un puntatore all’inizio dell’array, no