1. Introduzione a array-to-pointer conversion (decay)
Con gli array in C++ c'è una sottigliezza storica: il linguaggio supporta uno stile dei tempi in cui molte API volevano ricevere "solo l'indirizzo dei dati". Per questo motivo, nelle espressioni l'array spesso si comporta non come un "contenitore di N elementi", ma come "l'indirizzo del primo elemento". Questa trasformazione si chiama array-to-pointer conversion o decay (nel parlato — "l'array è decaduto a puntatore").
Perché è stato fatto così? Principalmente per comodità e compatibilità con il linguaggio C: era più semplice per funzioni e interfacce vecchie lavorare con "l'inizio dei dati". Ma la comodità ha un costo: l'indirizzo non conserva la dimensione, mentre l'array ha la dimensione — una delle parti fondamentali del suo significato.
L'idea chiave per tutta la lezione: "array come oggetto" e "nome dell'array in un'espressione" non sono la stessa cosa, e confonderle rompe la logica del programma.
Array come oggetto: memoria, elementi e dimensione
È importante mettere le idee in ordine: un array è un oggetto. Occupa memoria per tutti gli elementi insieme e ha una lunghezza fissa, nota a tempo di compilazione.
Sviluppiamo il nostro esempio: una tabella dei punteggi (per esempio, punti per 5 round).
#include <cstddef>
#include <iostream>
int main() {
constexpr std::size_t N = 5;
int scores[N] = {10, 12, 8, 15, 9};
std::cout << scores[0] << ' ' << scores[N - 1] << '\n'; // 10 9
}
Finché accedete a scores[i] e usate N, tutto è trasparente: c'è la lunghezza, ci sono gli elementi, ci sono i limiti corretti 0..N-1.
Finché trattate l'array "come array", potete calcolare in sicurezza il numero di elementi, fare sizeof(scores), usare std::size(scores) e in generale sentirvi sicuri.
Modello minimo del puntatore: "puntatore = indirizzo"
La parola "puntatore" suona come l'inizio di storie paurose su memory leak, new/delete e incubi notturni del debugger. Qui non parleremo di questo: oggi il puntatore è semplicemente una variabile che contiene un indirizzo, cioè "la posizione in memoria" dove si trova un oggetto.
Una semplice analogia: l'array è una "mensola con libri", il puntatore è un "etichetta con la coordinata dove sta il primo libro". L'etichetta è utile, ma se su di essa non è scritto quanti libri ci sono sulla mensola, potreste provare a prendere "il sesto libro" dove la mensola è già finita.
La pratica minima che ci serve: se p è un puntatore a int, l'espressione p[0] significa "il primo int a quell'indirizzo". Non discutiamo il perché del comportamento e non facciamo aritmetica di puntatori — accettiamo semplicemente che in C++ si possa accedere ai dati tramite un indirizzo in questo modo.
2. Decay in pratica: l'array si trasforma nell'indirizzo del primo elemento
Ora — il fulcro della lezione. Per l'array int scores[N] esiste una proprietà importante:
- scores come oggetto è un array di N elementi;
- ma scores nella maggior parte delle espressioni si trasforma automaticamente in int*, cioè nell'indirizzo del primo elemento.
Praticamente si presenta così:
#include <cstddef>
#include <iostream>
int main() {
constexpr std::size_t N = 5;
int scores[N] = {10, 12, 8, 15, 9};
int* p = scores; // decay: p "punta" a scores[0]
std::cout << scores[0] << ' ' << p[0] << '\n'; // 10 10
}
Qui è avvenuta la trasformazione "array → indirizzo del primo elemento". Il puntatore p punta allo stesso luogo dove si trova scores[0].
Se volete vedere questo in modo esplicito, potete prendere direttamente l'indirizzo del primo elemento:
#include <cstddef>
#include <iostream>
int main() {
constexpr std::size_t N = 5;
int scores[N] = {10, 12, 8, 15, 9};
int* p1 = scores; // implicitamente (decay)
int* p2 = &scores[0]; // esplicitamente
std::cout << p1 << '\n'; // ad esempio: 0x7ff... (l'indirizzo sarà diverso sul vostro sistema)
std::cout << p2 << '\n'; // lo stesso indirizzo
}
std::cout per int* stampa l'indirizzo (di solito in esadecimale). Il formato preciso dipende dall'implementazione, ma il senso è uno: è una "coordinata in memoria".
5. Il prezzo del decay: la dimensione è persa
Perché non si può "ottenere" la dimensione da T*
Quando avevate l'array scores[N], conoscevate N. Quando vi è rimasto solo int* p, conoscete solo "dove si trova il primo elemento". Ma non sapete quante posizioni dopo appartengono ancora al vostro array e quante sono "altrui".
Il puntatore non è un contenitore né una "struttura intelligente". È letteralmente un indirizzo. Perciò la richiesta "dammi la dimensione dell'array a cui punta p" è concettualmente impossibile: il linguaggio non memorizza la "dimensione dell'array" dentro int*.
Per capire il problema praticamente, facciamo una dimostrazione di "cattiva idea": provare a calcolare il numero di elementi usando sizeof sul puntatore.
#include <cstddef>
#include <iostream>
int main() {
constexpr std::size_t N = 5;
int scores[N] = {10, 12, 8, 15, 9};
int* p = scores;
std::size_t wrongCount = sizeof(p) / sizeof(p[0]);
std::cout << wrongCount << '\n'; // spesso 2 su 64-bit (8 / 4), ma questo NON è la dimensione dell'array
}
Questo risultato non ha a che fare con il numero di elementi. Ha a che fare con il fatto che la "dimensione dell'indirizzo" (per esempio 8 byte) viene divisa per la "dimensione di int" (per esempio 4 byte). È matematica casuale, non logica del programma.
sizeof(array) e sizeof(puntatore) — entità diverse
sizeof misura la dimensione del tipo/oggetto in byte. E qui array e puntatore differiscono radicalmente:
- sizeof(scores) — la dimensione dell'intero array (tutti gli elementi insieme);
- sizeof(p) — la dimensione della variabile-indirizzo (di solito 4 o 8 byte), ma non dei dati.
#include <cstddef>
#include <iostream>
int main() {
constexpr std::size_t N = 5;
int scores[N] = {10, 12, 8, 15, 9};
int* p = scores;
std::cout << "sizeof(scores) = " << sizeof(scores) << '\n';
std::cout << "sizeof(p) = " << sizeof(p) << '\n';
}
Immagine tipica su un sistema a 64 bit: sizeof(scores) sarà 20 (5 * 4), mentre sizeof(p) sarà 8. Ma non fissatevi sui numeri: anche se int avesse una dimensione diversa, la logica resta — l'array è "pesante", il puntatore è "leggero".
Come ottenere correttamente la lunghezza dell'array
I principianti spesso memorizzano la formula:
sizeof(a) / sizeof(a[0])
Funziona davvero... finché a è un array. Appena a "decade" a puntatore, state già dividendo "la dimensione dell'indirizzo" per "la dimensione dell'elemento" e ottenete una nonsense.
#include <cstddef>
#include <iostream>
int main() {
int a[5] = {1, 2, 3, 4, 5};
int* p = a;
std::size_t ok = sizeof(a) / sizeof(a[0]);
std::size_t bad = sizeof(p) / sizeof(p[0]);
std::cout << ok << ' ' << bad << '\n'; // 5 2 (bad — logica errata)
}
Un modo più leggibile per ottenere il numero di elementi di un array è std::size(a). Capisce che gli è stato passato un array e restituisce il numero corretto.
#include <cstddef>
#include <iostream>
#include <iterator> // std::size
int main() {
int a[5] = {1, 2, 3, 4, 5};
int* p = a;
std::cout << std::size(a) << '\n'; // 5
// std::cout << std::size(p) << '\n'; // non si può fare: p non è un array, non ha lunghezza
(void)p;
}
6. Pratica: output e iterazione tramite puntatore
Perché std::cout << a; non stampa gli elementi
C'è un momento comico che cattura quasi tutti: fate std::cout << a; e vi aspettate di vedere 1 2 3 4 5, ma ottenete qualcosa come 0x7ff....
La ragione è la stessa: nell'espressione a essa decade a int*, e l'operator<< per puntatori stampa l'indirizzo.
#include <iostream>
int main() {
int a[3] = {10, 20, 30};
std::cout << a << '\n'; // 0x7ff... (indirizzo, NON "10 20 30")
std::cout << a[0] << '\n'; // 10
}
Se volete stampare gli elementi, iterate con un ciclo (per ora senza algoritmi e senza range-for, per non anticipare argomenti):
#include <cstddef>
#include <iostream>
int main() {
constexpr std::size_t N = 3;
int a[N] = {10, 20, 30};
for (std::size_t i = 0; i < N; ++i) {
std::cout << a[i] << ' ';
}
std::cout << '\n'; // 10 20 30
}
Se vi resta solo T*, conservate la lunghezza separatamente
Regola pratica che salva nervi: non appena fate "il passo verso il puntatore", accanto deve esserci la lunghezza.
In questa lezione lo facciamo in modo diretto: c'è N, c'è p, e attraversiamo p[i], ma il limite del ciclo lo prendiamo da N, non proviamo ad indovinare.
#include <cstddef>
#include <iostream>
int main() {
constexpr std::size_t N = 5;
int scores[N] = {10, 12, 8, 15, 9};
int* p = scores;
int sum = 0;
for (std::size_t i = 0; i < N; ++i) {
sum += p[i];
}
std::cout << "sum=" << sum << '\n'; // sum=54
}
Se volete rinforzare la disciplina (e ridurre la probabilità di "modificare accidentalmente N"), potete prendere la lunghezza dall'array usando std::size(scores), ma solo prima che abbiate "perso l'arrayness":
#include <cstddef>
#include <iostream>
#include <iterator>
int main() {
int scores[5] = {10, 12, 8, 15, 9};
const std::size_t n = std::size(scores);
int* p = scores;
for (std::size_t i = 0; i < n; ++i) {
std::cout << p[i] << ' ';
}
std::cout << '\n'; // 10 12 8 15 9
}
Schema: cosa succede esattamente durante il decay
Per non trasformare questo in un "rito magico del compilatore", è utile tenere a mente uno schema semplice: c'è l'oggetto-array, e c'è l'espressione che spesso si trasforma in indirizzo.
flowchart TD
A["int scores[N] (array come oggetto)
ha N elementi"] --> B["scores (nell'espressione)"]
B --> C["int* p = scores;
p contiene l'indirizzo di scores[0]"]
C --> D["p[i] (accesso ai dati tramite indirizzo)
MA i deve essere limitato separatamente"]
Il senso di questo schema è: array e puntatore sono collegati, ma non sono la stessa cosa.
7. Errori tipici
Errore #1: pensare che T* "conosca la lunghezza dell'array".
Dopo int* p = a; molti si aspettano intuitivamente che p contenga non solo l'indirizzo ma anche la dimensione. Ma la dimensione dell'array non fa parte del puntatore. Se iterate su p con un ciclo, il limite deve essere noto separatamente (costante N, valore di std::size(a) prima del decay, ecc.), altrimenti è facilissimo oltrepassare i limiti e ottenere comportamento indefinito.
Errore #2: calcolare il numero di elementi usando sizeof(p) / sizeof(p[0]).
Sembra logico finché non capite che sizeof(p) è la dimensione dell'indirizzo, non dei dati. Il numero risultante spesso sembra "plausibile" (per esempio 2), e per questo l'errore sopravvive. La formula corretta sizeof(a) / sizeof(a[0]) funziona solo per un vero array, dove a è un oggetto array e non un puntatore.
Errore #3: scrivere std::cout << a; aspettandosi gli elementi.
L'output del flusso vede non "l'array", ma "l'indirizzo del primo elemento" (risultato del decay), quindi stampa l'indirizzo. Per stampare gli elementi serve un ciclo e l'output esplicito di a[i]. Se vedete 0x... nell'output, non è che cout sia rotto: avete chiesto di stampare un indirizzo.
Errore #4: confondere "ultimo indice" e "dimensione".
Dopo il passaggio al puntatore è facile sbagliare i limiti: la dimensione è N, l'ultimo indice è N - 1. Un errore come i <= N nel ciclo si traduce nel leggere p[N], cioè il "sesto elemento" di un array di cinque. Anche se il programma "non crasha", non significa che sia corretto: potrebbe silenziosamente leggere memoria altrui.
Errore #5: giustificare p[i] con l'aritmetica dei puntatori e subito dopo iniziare a sperimentare.
Sì, i puntatori hanno aritmetica, ma è un argomento a parte con le sue insidie. In questa lezione mantenete il modello minimo: "puntatore = indirizzo", "p[i] dà accesso all'i-esimo elemento", "i limitatelo con una dimensione nota separatamente". Questo fornirà una base solida e impedirà che l'apprendimento si trasformi in una safari nel comportamento indefinito.
GO TO FULL VERSION