I puntatori contengono indirizzi, e gli indirizzi sono numeri: è ragionevole aspettarsi che l'aritmetica funzioni anche su di loro. Funziona, ma non proprio come l'aritmetica degli interi. Il C scala ogni operazione sui puntatori in base alla dimensione dell'oggetto puntato, e questo trasforma "aggiungi uno" nel molto più utile "passa all'elemento successivo".
È grazie a quest'unica scelta di progetto che in C percorrere un array con un puntatore è naturale quanto percorrerlo con un indice.
Sommare 1 sposta di un oggetto, non di un byte
Prendi un puntatore, aggiungi 1 e stampa entrambi gli indirizzi:
L'int * salta di 4 byte, il double * di 8, il char * di 1: ognuno atterra esattamente dove inizierebbe il prossimo oggetto del suo tipo. La regola è semplice:
ptr + n == (indirizzo in ptr) + n * sizeof(*ptr)
È proprio per questo che un puntatore ha un tipo. Un indirizzo nudo, senza tipo, non avrebbe idea di quanto sia lontano il "successivo".
Percorrere un array con un puntatore
Ora il vantaggio. Gli elementi di un array stanno in memoria consecutiva, quindi far avanzare un puntatore li visita in ordine:
Entrambi i cicli stampano gli stessi cinque numeri. Nel secondo, scores decade a un puntatore al suo primo elemento, p++ avanza di un int e *p legge l'elemento che si trova lì. Disposto in memoria:
scores[0] scores[1] scores[2] scores[3] scores[4]
+---------+---------+---------+---------+---------+
| 88 | 92 | 75 | 60 | 100 |
+---------+---------+---------+---------+---------+
^ ^ ^
p p+1 scores+5
(inizio) (4 byte dopo) (uno oltre la fine)
La relazione tra le due forme è esatta e vale la pena impararla a memoria: scores[i] è definito come *(scores + i). Se ne parla di più in puntatori e array.
Incremento, decremento e la trappola di *p++
++ e -- funzionano sui puntatori con la stessa scalatura.
L'espressione che confonde tutti è *p++. Il ++ postfisso si lega più strettamente di *, quindi significa "prendi il p attuale, fai avanzare p, poi dereferenzia il vecchio valore":
Tre espressioni, tre significati diversi:
| Espressione | Puntatore dopo | Valore prodotto |
|---|---|---|
*p++ | avanzato | l'elemento prima dello spostamento |
*++p | avanzato | l'elemento dopo lo spostamento |
(*p)++ | invariato | il vecchio valore, e l'elemento viene incrementato |
*p++ è C idiomatico, è così che si scrivono le copie di stringhe e buffer, ma metti le parentesi quando intendi qualsiasi altra cosa.
Sottrarre due puntatori
Sottrarre un puntatore da un altro dà il numero di elementi tra i due, non il numero di byte:
Il tipo del risultato è ptrdiff_t, un tipo intero con segno di <stddef.h>, che si stampa con %td. Sottrarre puntatori a due array diversi produce un risultato indefinito: lo standard definisce l'operazione solo all'interno di un unico oggetto.
Puoi anche sottrarre un intero da un puntatore (p - 3 torna indietro di tre elementi), ma non puoi sommare due puntatori. "La somma di due indirizzi" non indica niente.
Confrontare i puntatori
Gli operatori relazionali funzionano su puntatori dentro lo stesso array, e significano esattamente ciò che suggerisce la disposizione in memoria: p < q è vero quando p indica un elemento precedente.
front < back è tutta la condizione di uscita del ciclo, ed è corretta perché entrambi i puntatori restano dentro lo stesso array. Anche == e != sono il modo naturale di confrontare un ciclo con il suo marcatore di fine, e questo ci porta alla regola che rende lecito tutto questo.
La regola dell'elemento oltre la fine
Il C permette esplicitamente di formare un puntatore alla posizione subito dopo l'ultimo elemento di un array. Puoi calcolarlo, memorizzarlo e fare confronti con esso. Non puoi dereferenziarlo.
int arr[4];
arr[0] arr[1] arr[2] arr[3] (nessun elemento)
+------+ +------+ +------+ +------+ +- - - -+
| | | | | | | | | |
+------+ +------+ +------+ +------+ +- - - -+
^ ^
arr arr + 4
lecito da usare lecito FORMARLO e CONFRONTARLO
mai lecito dereferenziarlo
È questa regola che rende valida la forma standard del ciclo:
for (int *p = arr; p != arr + n; p++) {
/* ... */
}
All'ultima iterazione p diventa arr + n, il confronto fallisce e il ciclo esce, senza mai leggere quella posizione.
Due cose restano comunque indefinite. Formare un puntatore due posizioni oltre la fine, o una prima dell'inizio, è comportamento indefinito anche se non lo dereferenzi mai:
int *bad1 = arr + n + 1; // indefinito: troppo oltre la fine
int *bad2 = arr - 1; // indefinito: prima dell'inizio
Il secondo conta nella pratica: un ciclo all'indietro scritto come for (int *p = arr + n - 1; p >= arr; p--) calcola arr - 1 al suo ultimo decremento. Funziona su tutti i compilatori comuni ed è comunque formalmente indefinito. Il ciclo all'indietro pulito lo evita:
Puntatori void e l'aritmetica che non puoi fare
Un void * contiene un indirizzo senza tipo, ed è così che malloc e qsort restano generiche. Dato che non c'è una dimensione dell'elemento, lo standard non permette l'aritmetica su un void *: prima fai il cast a un tipo concreto (o a char * per lavorare a livello di byte).
void *v = buffer;
// v + 1; // non e' C standard: nessuna dimensione per cui scalare
char *b = v;
b + 1; // va bene: un byte in avanti
GCC e Clang accettano l'aritmetica su void * come estensione che la tratta come char *, quindi il codice che ci si affida compila fino al giorno in cui viene compilato da qualche parte più rigorosa. Compila con -std=c17 -pedantic se vuoi che te lo dicano.
Cosa ti dà tutto questo
L'aritmetica dei puntatori non è un trucco di ottimizzazione da tirare fuori ogni tanto: è il meccanismo sotto l'indicizzazione degli array, la gestione delle stringhe e ogni scansione di buffer della libreria standard. strlen è un puntatore fatto avanzare fino al '\0' finale e sottratto dall'inizio. memcpy sono due puntatori che avanzano di pari passo.
Poi guarda come funziona davvero l'equivalenza tra array e puntatori, compresa la trappola di sizeof che colpisce tutti la prima volta che passano un array a una funzione: puntatori e array.
Domande frequenti
Cosa fa ptr + 1 in C?
Sposta il puntatore in avanti di un oggetto, non di un byte. Per un int * su una macchina tipica sono 4 byte; per un double * sono 8. Il compilatore moltiplica per sizeof(*ptr) al posto tuo, ed è per questo che il tipo del puntatore conta.
Si possono sottrarre due puntatori in C?
Sì, quando entrambi puntano dentro lo stesso array. end - start dà il numero di elementi tra i due, con tipo ptrdiff_t (stampalo con %td). Sottrarre puntatori a oggetti diversi è comportamento indefinito.
È lecito puntare subito oltre la fine di un array?
Sì: formare un puntatore all'elemento successivo all'ultimo è esplicitamente permesso, così che cicli come for (int *p = arr; p != arr + n; p++) funzionino. Puoi calcolare e confrontare quel puntatore, ma dereferenziarlo è comportamento indefinito.
Perché *p++ non è la stessa cosa di (*p)++?
*p++ incrementa il puntatore e dereferenzia il vecchio valore: legge l'elemento corrente, poi avanza. (*p)++ lascia stare il puntatore e incrementa il valore a cui punta. Il ++ postfisso si lega più strettamente di *, quindi sono le parentesi a cambiare il significato.