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.