Puntatori: indirizzi, dereferenziazione e aritmetica
Comprendi i puntatori come indirizzi di memoria, impara gli operatori di indirizzo e di dereferenziazione, l'aritmetica dei puntatori all'interno degli array, la correttezza const con i puntatori e quando i puntatori grezzi sono ancora appropriati nel C++ moderno.
Che cos'è un puntatore?
Un puntatore è una variabile che memorizza un indirizzo di memoria. Ogni oggetto in C++ risiede a un certo indirizzo in memoria, e un puntatore ti permette di riferirti a quell'oggetto in modo indiretto. I puntatori sono fondamentali in C++ — rendono possibili le strutture dati dinamiche, il polimorfismo e il passaggio efficiente dei parametri.
Un tipo puntatore si dichiara con *: int* p; dichiara un puntatore a un int. Il puntatore stesso occupa memoria (tipicamente 8 byte sui sistemi a 64 bit) e il suo valore è un indirizzo, non il dato a cui punta.
Operatore di indirizzo e dereferenziazione
I due operatori fondamentali sui puntatori sono & (indirizzo di) e * (dereferenziazione). L'operatore & ottiene l'indirizzo di una variabile. L'operatore * segue l'indirizzo per accedere all'oggetto puntato.
#include <iostream>
int main() {
int value = 42;
int* ptr = &value; // ptr holds the address of value
std::cout << "value: " << value << '\n'; // 42
std::cout << "&value: " << &value << '\n'; // e.g. 0x7ffd5a3c
std::cout << "ptr: " << ptr << '\n'; // same address
std::cout << "*ptr: " << *ptr << '\n'; // 42 (dereference)
*ptr = 100; // modify value through the pointer
std::cout << "value after *ptr = 100: " << value << '\n'; // 100
int* null_ptr = nullptr; // points to nothing — safe sentinel
// *null_ptr = 5; // UNDEFINED BEHAVIOR — crash on most systems
}Aritmetica dei puntatori e decadimento degli array
L'aritmetica dei puntatori è valida solo all'interno di un array (o un elemento oltre la fine). Il compilatore scala l'aritmetica in base a sizeof(T). Nella maggior parte dei contesti il nome di un array decade in un puntatore al suo primo elemento — ecco perché gli array in stile C perdono l'informazione sulla dimensione quando vengono passati alle funzioni.
#include <iostream>
#include <cstddef> // std::ptrdiff_t
int main() {
int arr[5] = {10, 20, 30, 40, 50};
int* p = arr; // array decays to pointer to first element
std::cout << *p << '\n'; // 10
std::cout << *(p + 2) << '\n'; // 30 (moves 2*sizeof(int) bytes)
std::cout << p[3] << '\n'; // 40 (p[i] is *(p + i))
// Iterating via pointer
for (int* it = arr; it != arr + 5; ++it) {
std::cout << *it << ' ';
}
std::cout << '\n'; // 10 20 30 40 50
// Pointer difference
std::ptrdiff_t diff = (arr + 4) - arr; // 4 (elements, not bytes)
std::cout << "diff: " << diff << '\n';
// WARNING: arithmetic outside the array is UNDEFINED BEHAVIOR
// int* bad = arr + 10; // UB — don't do this
}Puntatori const vs puntatori a const
La posizione di const rispetto a * determina che cosa è immutabile:
- const int* p (oppure int const* p) — puntatore a const: non puoi modificare il valore puntato attraverso p, ma puoi far puntare p altrove.
- int* const p — puntatore const: il puntatore stesso è immutabile (non puoi farlo puntare altrove), ma puoi modificare il valore a cui punta.
- const int* const p — sia il puntatore sia il valore puntato sono immutabili.
Leggi le dichiarazioni da destra a sinistra: int* const p si legge come "p è un puntatore const a int".
I puntatori void (void*) possono contenere qualsiasi indirizzo ma non possono essere dereferenziati senza un cast. Si usano soprattutto nelle API in stile C e nelle implementazioni degli allocatori.
Nel C++ moderno i puntatori grezzi dovrebbero essere usati esclusivamente come puntatori osservatori, non proprietari. Dicono: "posso vedere questo oggetto, ma non sono responsabile della sua durata di vita". Usi legittimi comuni:
- Implementare strutture dati (nodi di un albero che puntano al genitore)
- Parametri di funzione che osservano ma non possiedono (anche se spesso i riferimenti sono preferibili)
- Interfacciarsi con API C
- Iteratori in contenitori personalizzati
Se un puntatore possiede una risorsa, incapsulalo in std::unique_ptr o std::shared_ptr. Se ti ritrovi a scrivere delete, nel C++ moderno quasi sicuramente stai sbagliando qualcosa.
I puntatori pendenti (dangling) sono il bug numero uno legato ai puntatori. Un puntatore diventa pendente quando l'oggetto a cui punta viene distrutto:
- Restituire un puntatore a una variabile locale
- Distruggere un oggetto con delete mentre un altro puntatore lo referenzia ancora
- Un puntatore all'interno di un std::vector che viene riallocato dopo un push_back
I puntatori non inizializzati contengono indirizzi spazzatura. Inizializzali sempre a nullptr se non hai ancora un obiettivo. Il compilatore NON ti avviserà di tutti gli usi di puntatori non inizializzati.
Puntatore vs riferimento: quale usare
Riferimenti e puntatori forniscono entrambi un livello di indirezione, ma differiscono in aspetti importanti. I riferimenti non possono essere null, non possono essere riassegnati a un altro oggetto e non richiedono dereferenziazione esplicita. Preferisci i riferimenti per i parametri di funzione; usa i puntatori quando ti serve la possibilità di essere null o di cambiare oggetto puntato.
#include <iostream>
void increment_ref(int& val) { ++val; } // cannot be null
void increment_ptr(int* val) { if (val) ++(*val); } // must null-check
int main() {
int x = 10;
increment_ref(x); // clean call syntax
increment_ptr(&x); // caller must take address
std::cout << x << '\n'; // 12
int* p = &x;
p = nullptr; // OK — pointers can be re-seated and nulled
int& r = x;
// r = ???; // references cannot be re-seated
// int& bad; // ERROR — references must be initialized
}&ottiene un indirizzo,*lo segue — sono operazioni inverse- L'aritmetica dei puntatori è valida solo all'interno degli array e scala in base a
sizeof(T) - I nomi degli array decadono in puntatori, perdendo l'informazione sulla dimensione
- Leggi la posizione di
constda destra a sinistra:const int*vsint* const - Nel C++ moderno i puntatori grezzi dovrebbero essere solo osservatori non proprietari
- Preferisci i riferimenti ai puntatori, a meno che non ti serva un valore null o la possibilità di cambiare oggetto puntato
Quiz — Metti alla prova le tue conoscenze
(15 XP)1. Che cosa significa `const int* p`?
2. Che cosa succede quando un array viene passato a una funzione che si aspetta un parametro puntatore?
3. Quando è valida l'aritmetica dei puntatori in C++?