Come molti altri linguaggi, il C++ ha i puntatori. Sai già che riferimenti e puntatori sono simili, ma pensali come un livello più vicino al funzionamento interno del computer. I puntatori sono variabili che contengono indirizzi di oggetti. Si usano per interagire direttamente con gli oggetti, consentendo l'allocazione dinamica della memoria e una manipolazione efficiente dei dati in C++.
Se è la prima volta che incontri i puntatori, possono sembrare un po' misteriosi, ma una volta che ci prendi la mano sono piuttosto semplici.
Sono una parte cruciale del C++, quindi prenditi il tempo necessario per capirli davvero. La versione essenziale presentata in questo concetto è anche chiamata puntatore stupido o puntatore grezzo. Nel C++ moderno esistono anche i puntatori intelligenti; il tipo di base non è affatto intelligente e devi gestire tutto il lavoro manualmente.
Prima di addentrarci nei dettagli, vale la pena capire a cosa servono i puntatori. I puntatori sono un modo per condividere l'indirizzo di un oggetto con altre parti del programma, il che è utile per due motivi principali:
La dichiarazione di un puntatore in C++ prevede di specificare il tipo di dato a cui il puntatore punta, seguito da un asterisco (*) e dal nome del puntatore.
Quando si dichiarano i puntatori, questi non vengono inizializzati automaticamente.
Senza un'assegnazione esplicita, un puntatore contiene in genere un valore indeterminato, spesso indicato come «indirizzo spazzatura».
Anche se alcuni compilatori potrebbero inizializzare i puntatori a nullptr, questo comportamento non è garantito su tutti i compilatori, quindi è essenziale non farci affidamento.
La buona pratica è inizializzare esplicitamente i puntatori grezzi e verificarne lo stato non nullo prima di usarli, per evitare potenziali problemi.
int* ptr{nullptr}; // Declares a pointer and makes sure it is not invalid
Per assegnare l'indirizzo di una variabile a un puntatore, si usa l'operatore di indirizzo (&).
La dereferenziazione di un puntatore avviene tramite l'operatore di indirezione (*).
std::string opponent{"Solomon Lane"};
// 'ethan' points to the address of the string opponent
std::string* ethan{&opponent};
// Instead of ethan's, the opponent's name address is given to the passPort
std::string passportName{*ethan};
Attenzione: la dereferenziazione va fatta esplicitamente, mentre i riferimenti funzionano semplicemente come un alias.
L'aritmetica dei puntatori permette di eseguire operazioni aritmetiche sui puntatori, cosa particolarmente utile quando si lavora con gli array. Aggiungere un numero intero a un puntatore lo fa puntare a un elemento diverso.
// Stargate Coordinate Code
int gateCode[] = {462, 753, 218, 611, 977};
// 'ptr' points to the first element of 'gateCode'
int* ptr{&gateCode[0]};
// Accesses the third Stargate address through pointer arithmetic
int dialedAddress{*(ptr + 2)};
// Chevron encoded! Dialing Stargate address:
openStarGate(dialedAddress);
L'aritmetica dei puntatori in C++ può facilmente portare a un comportamento indefinito se non viene gestita con attenzione. Il comportamento indefinito può manifestarsi in risultati inattesi del programma, arresti anomali o persino vulnerabilità di sicurezza. Un famoso esempio delle conseguenze del comportamento indefinito si è verificato nel 1996 con l'esplosione del razzo Ariane 5, dove un'eccezione software causata dalla conversione di un numero in virgola mobile a 64 bit in un intero con segno a 16 bit portò a un guasto catastrofico.
In C++, l'operatore -> serve ad accedere ai membri di un oggetto tramite un puntatore a quell'oggetto.
È una scorciatoia che semplifica l'accesso ai membri degli oggetti puntati dai puntatori.
Per esempio, se ptr è un puntatore a un oggetto con una variabile membro x, invece di usare (*ptr).x puoi usare direttamente ptr->x.
Questo operatore migliora la leggibilità del codice e riduce la verbosità quando si lavora con puntatori a oggetti.
Ecco un breve esempio, con una struct Superhero che ha una variabile membro superpower.
La funzione main crea un puntatore dianaPrince a un oggetto Superhero (che rappresenta Wonder Woman).
L'operatore -> serve ad accedere alla variabile membro superpower, mostrando il famoso «Lasso of Truth» di Wonder Woman.
struct Superhero {
std::string superpower;
};
Superhero wonder_woman{};
Superhero* dianaPrince = &wonder_woman;
dianaPrince->superpower = "Lasso of Truth";
// Using the -> operator to access member variable superpower:
std::cout << "Wonder Woman, possesses the mighty " << dianaPrince->superpower;
// Memory cleanup:
delete dianaPrince;
Sia i puntatori che i riferimenti consentono un accesso indiretto agli oggetti, ma differiscono per capacità e considerazioni sulla sicurezza. I puntatori offrono la flessibilità di cambiare l'oggetto a cui puntano e possono essere impostati a null. Tuttavia, questa flessibilità introduce dei rischi, come dereferenziare puntatori nulli o creare puntatori penzolanti. I riferimenti, invece, non possono essere nulli e sono legati a oggetti validi al momento della creazione, evitando questi rischi. Data la loro natura più sicura, i riferimenti dovrebbero essere preferiti ai puntatori, a meno che non siano necessarie le funzionalità aggiuntive offerte dai puntatori.
In C++, new serve ad allocare dinamicamente memoria per gli oggetti sullo heap durante l'esecuzione.
Restituisce un puntatore all'indirizzo di memoria allocato, permettendo agli oggetti di avere una durata che non è legata a uno scope specifico.
Tuttavia, è importante abbinare sempre new a delete per deallocare correttamente la memoria.
L'allocazione dinamica della memoria va usata con cautela per evitare problemi comuni come le perdite di memoria e l'accesso a memoria deallocata.
I puntatori possono anche essere impostati a un valore speciale chiamato puntatore nullo, che indica che non puntano a un indirizzo valido. I puntatori nulli sono comunemente usati per inizializzare i puntatori prima di assegnarli a valori validi.
// There is a dark mark bearer, but who is it?
std::string* darkMarkBearer{nullptr};
// It is Bellatrix!
darkMarkBearer = new std::string{"Bellatrix Lestrange"};
// She has been defeated
delete darkMarkBearer;
// Reset pointer to null, symbolizing there is no more darkMarkBearer at the moment
darkMarkBearer = nullptr;
Un puntatore dovrebbe contenere l'indirizzo di un oggetto valido oppure essere impostato a nullptr.
Così devi solo verificare se un puntatore è nullo e puoi presupporre che ogni puntatore non nullo sia valido.
Quando un oggetto viene distrutto, tutti i puntatori a quell'oggetto restano penzolanti (non vengono impostati automaticamente a nullptr).
È tua responsabilità individuare questi casi e assicurarti che quei puntatori vengano poi impostati a nullptr.
Nel codice più vecchio potresti incontrare due alternative a nullptr.
In primo luogo, il letterale 0 è interpretato specificamente come valore nullo per i puntatori, anche se è l'unico caso in cui un letterale intero può essere assegnato a un puntatore.
In secondo luogo, la preprocessor macro NULL, ereditata dal C e definita nell'header <cstddef>, è un'altra rappresentazione di un puntatore nullo, anche se il suo uso è meno comune nel codice C++ moderno.