Puntatori, riferimenti e memoria dinamica in C++

Puntatori, riferimenti e memoria dinamica in C++

I puntatori sono una delle caratteristiche più potenti e, allo stesso tempo, più delicate del C++. Un puntatore è una variabile che non contiene un valore diretto, ma l'indirizzo in memoria di un altro valore. Comprendere i puntatori significa comprendere come il programma organizza i dati in memoria, ed è il prerequisito per affrontare la gestione dinamica della memoria e molte tecniche avanzate del linguaggio.

Indirizzi e puntatori

Ogni variabile risiede in una cella di memoria identificata da un indirizzo. L'operatore &, applicato a una variabile, ne restituisce l'indirizzo. Un puntatore si dichiara aggiungendo un asterisco al tipo, e può contenere l'indirizzo di una variabile dello stesso tipo. L'operatore di dereferenziazione, anch'esso indicato con l'asterisco, permette di accedere al valore puntato.

#include <iostream>

int main() {
    int value = 42;
    int* pointer = &value;   // pointer contiene l'indirizzo di value

    std::cout << *pointer << std::endl;   // Dereferenziazione: stampa 42

    *pointer = 100;   // Modifica value attraverso il puntatore
    std::cout << value << std::endl;      // Ora value vale 100
    return 0;
}

Il puntatore nullo

Un puntatore che non punta a nulla dovrebbe contenere il valore speciale nullptr, introdotto dal C++ moderno per sostituire le vecchie convenzioni meno sicure. Verificare che un puntatore non sia nullo prima di dereferenziarlo è essenziale, perché dereferenziare un puntatore nullo provoca il crash del programma.

int* pointer = nullptr;   // Puntatore che non punta a nulla

if (pointer != nullptr) {
    std::cout << *pointer << std::endl;   // Sicuro solo se non nullo
}

Differenza tra puntatori e riferimenti

Un riferimento, che abbiamo già incontrato con il simbolo &, è un alias di una variabile esistente. A differenza di un puntatore, un riferimento deve essere inizializzato al momento della dichiarazione, non può essere nullo e non può essere riassegnato per riferirsi a un'altra variabile. I riferimenti sono più semplici e sicuri, e si preferiscono quando non serve la flessibilità dei puntatori; i puntatori restano indispensabili quando occorre rappresentare l'assenza di un valore o riallocare a runtime.

int original = 10;

int& alias = original;   // alias e un altro nome per original
alias = 20;             // Modifica anche original

int* ptr = &original;   // ptr puo essere riassegnato e puo essere nullo

Memoria dinamica con new e delete

Finora le variabili sono state allocate automaticamente, vivendo solo all'interno dell'ambito in cui erano dichiarate. A volte, però, serve creare oggetti la cui durata non dipende dall'ambito corrente. A questo scopo il C++ offre l'allocazione dinamica tramite l'operatore new, che riserva memoria e restituisce un puntatore all'oggetto creato. La memoria così allocata deve essere rilasciata esplicitamente con l'operatore delete, altrimenti rimane occupata fino al termine del programma, generando una perdita di memoria.

int* number = new int(7);   // Alloca un intero sullo heap

std::cout << *number << std::endl;

delete number;          // Rilascia la memoria allocata
number = nullptr;       // Evita un puntatore pendente

Per gli array allocati dinamicamente si usano le forme con parentesi quadre, new[] e delete[], che devono sempre essere abbinate. Mescolare le due forme produce comportamenti indefiniti.

int* numbers = new int[5];   // Alloca un array di cinque interi

for (int i = 0; i < 5; i++) {
    numbers[i] = i * i;
}

delete[] numbers;        // Rilascia l'intero array

I pericoli della gestione manuale

La gestione manuale della memoria è una fonte notevole di errori. Dimenticare un delete causa una perdita di memoria; eseguirlo due volte sullo stesso puntatore provoca un comportamento indefinito; usare un puntatore dopo averne liberato la memoria genera un puntatore pendente. Questi problemi possono manifestarsi in modo intermittente e sono difficili da diagnosticare.

Proprio per superare queste insidie, il C++ moderno scoraggia l'uso diretto di new e delete nel codice applicativo, a favore degli smart pointer, che automatizzano il rilascio della memoria seguendo il principio della gestione delle risorse legata al ciclo di vita degli oggetti. Sarà l'argomento del prossimo articolo.