Vai al contenuto

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.

address_deref.cpp
#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.

pointer_arithmetic.cpp
#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.

Buona pratica

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.

Insidia

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.

ptr_vs_ref.cpp
#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
}
Concetti chiave
  • & 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 const da destra a sinistra: const int* vs int* 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++?