Puntatori
Indirizzi di memoria delle variabili
Ogni variabile memorizzata nella memoria di un computer occupa una specifica posizione chiamata indirizzo di memoria. Questo indirizzo è rappresentato come un numero intero che identifica univocamente la locazione nella RAM in cui è memorizzata la variabile.
Cosa significa indirizzo di memoria
- Quando dichiariamo una variabile in C++, ad esempio:
il compilatore riserva una zona di memoria per la variabileint numero = 42;numero. Questa zona è identificata da un indirizzo univoco, che possiamo ottenere utilizzando l’operatore di indirizzo&:
Il risultato sarà un valore esadecimale che rappresenta l’indirizzo.cout << "Indirizzo di numero: " << &numero << endl;
Indirizzo e dimensione delle variabili
La dimensione di memoria occupata da una variabile dipende dal suo tipo di dato. Ad esempio:
- Un
intoccupa generalmente 4 byte. - Un
charoccupa 1 byte. - Un
doubleoccupa 8 byte.
L’indirizzo rappresenta il primo byte della memoria occupata dalla variabile.
Cosa sono i puntatori
Un puntatore è una variabile speciale che contiene l’indirizzo di memoria di un’altra variabile. In altre parole, mentre le normali variabili memorizzano valori, i puntatori memorizzano indirizzi (che sono anche loro a tutti gli effetti dei valori).
Tipo e indirizzo dei puntatori

Anche un puntatore è una variabile, quindi:
- Un puntatore ha un indirizzo proprio, che può essere ottenuto con
&. - Un puntatore ha un tipo di dato, che dipende dal tipo della variabile a cui punta:
int numero = 42; int *ptr = № // 'ptr' è un puntatore a int
Nel codice sopra:
ptrmemorizza l’indirizzo dinumero.- Il tipo
int *specifica cheptrpunta a una variabile di tipoint.
Dimensione dei puntatori
La dimensione di un puntatore dipende dall’architettura del sistema:
- Su sistemi a 32 bit, un puntatore occupa 4 byte.
- Su sistemi a 64 bit, un puntatore occupa 8 byte. Indipendentemente dal tipo di variabile a cui punta, la dimensione del puntatore è sempre la stessa, perché rappresenta un indirizzo nella memoria e non il valore contnuto a quell’indirizzo.
Operazioni principali sui puntatori
Dichiarazione dei puntatori
Essendo una variabile, la dichiarazione di un puntatore segue le regole generali delle altre variabili, con l’aggiunta del simbolo * accanto al tipo:
int *ptr; // Dichiarazione di un puntatore a int
Il tipo della variabile include il simbolo * perché il puntatore “punta” a una variabile di un certo tipo. Ad esempio:
int *indica un puntatore a un intero (int).double *indica un puntatore a un numero in virgola mobile (double). È importante ricordare che un puntatore, essendo una variabile, ha un suo indirizzo di memoria e una sua dimensione, che dipende dall’architettura del sistema (es. 4 byte su sistemi a 32 bit, 8 byte su sistemi a 64 bit).
Assegnamento
Quando assegniamo un valore a un puntatore, gli stiamo dicendo quale indirizzo deve contenere, ovvero dove “puntare”. Per farlo, utilizziamo l’operatore di indirizzo &, che restituisce l’indirizzo di una variabile.
int numero = 42; // Dichiarazione di una variabile
int *ptr = № // Assegniamo a 'ptr' l'indirizzo di 'numero'
Qui, il puntatore ptr ora contiene l’indirizzo di memoria di numero. Questo significa che ptr sa dove trovare numero nella RAM. Possiamo verificare l’indirizzo:
cout << "Indirizzo di numero: " << &numero << endl;
cout << "Valore di ptr: " << ptr << endl; // Stampa lo stesso indirizzo
Metafora dell’assegnamento
Immagina di avere una casa (la variabile numero) e una mappa (il puntatore ptr):
- La casa ha un indirizzo specifico.
- Scrivendo l’indirizzo sulla mappa (
ptr = &numero), possiamo usarla per trovare la casa.
Dereferenziazione
La dereferenziazione è l’operazione che consente di accedere al valore memorizzato all’indirizzo contenuto nel puntatore. Per farlo, si utilizza l’operatore * davanti al puntatore.
int numero = 42;
int *ptr = №
cout << "Valore puntato da ptr: " << *ptr << endl; // Stampa 42
In questo esempio:
ptrcontiene l’indirizzo dinumero.- Usando
*ptr, seguiamo l’indirizzo per accedere al valore dinumero.
La dereferenziazione consente non solo di leggere, ma anche di modificare il valore della variabile puntata:
*ptr = 10; // Modifica il valore di 'numero' tramite il puntatore
cout << "Nuovo valore di numero: " << numero << endl; // Stampa 10
Metafora della dereferenziazione
Ritorniamo alla metafora della casa e della mappa:
- Se hai una mappa (
ptr) che punta all’indirizzo della casa (numero), puoi usarla per arrivare alla casa. - Una volta arrivato, puoi vedere cosa c’è dentro (leggere il valore con
*ptr) o sostituire l’arredamento (modificare il valore con*ptr = ...).
Una volta che dereferenziaziamo un puntatore, esso può funzionare esattamente come la variabile originale alla quale indica. Questo significa che possiamo leggere e modificare il valore della variabile originale tramite il puntatore dereferenziato esattamente come se stessimo lavorando nella variabile originale.
Esempio completo
Esempio di Assegnamento e Dereferenziazione
#include <iostream>
using namespace std;
int main() {
int numero = 5; // Variabile normale
int *ptr = № // Puntatore a 'numero'
cout << "Indirizzo di numero: " << &numero << endl;
cout << "Valore di numero: " << numero << endl;
// Usare il puntatore per accedere al valore di 'numero'
cout << "Valore puntato da ptr: " << *ptr << endl;
// Modificare 'numero' tramite il puntatore
*ptr = 20; // Cambia il valore di 'numero'
cout << "Nuovo valore di numero: " << numero << endl;
return 0;
}
Risultato atteso:
Indirizzo di numero: 0x7ffee4a2c4a8 (esempio, varia tra esecuzioni)
Valore di numero: 5
Valore puntato da ptr: 5
Nuovo valore di numero: 20
Errori comuni nell’uso dei puntatori
- Puntatori non inizializzati: Se un puntatore non è inizializzato (contiene “schifo”), punta a un indirizzo casuale:
int *ptr; // Non inizializzato *ptr = 10; // Errore: accesso a memoria non valida - Dereferenziare puntatori nulli: Un puntatore nullo (
nullptr) non può essere dereferenziato:int *ptr = nullptr; cout << *ptr; // Errore: crash dell'applicazione
Best practice:
- Inizializzare i puntatori a
nullptrse non si sa subito cosa devono puntare. - Controllare sempre se un puntatore è nullo prima di dereferenziarlo:
if (ptr != nullptr) { cout << *ptr << endl; }
Creare un albero genealogico in c++
Supponiamo di voler rappresentare un albero genealogico in un programma. Di ogni membro vogliamo memorizzare:
- Nome
- Cognome
- Data di nascita
- Genitori (padre e madre) Un’idea naturale sarebbe usare una struct per aggregare questi dati.
#include <iostream>
using namespace std;
struct Persona {
string nome;
string cognome;
string data_nascita;
};
int main() {
Persona persona = {"Mario", "Rossi", "01/01/1980"};
cout << "Nome: " << persona.nome << endl;
cout << "Cognome: " << persona.cognome << endl;
cout << "Data di nascita: " << persona.data_nascita << endl;
return 0;
}
Qui, la struct Persona funziona bene per aggregare nome, cognome e data di nascita. Tuttavia, se vogliamo aggiungere i genitori (anche loro di tipo Persona), non possiamo scrivere:
struct Persona {
string nome;
string cognome;
string data_nascita;
Persona padre; // Errore! Definizione ricorsiva della struct Persona
Persona madre; // Errore! Definizione ricorsiva
};
Non possiamo scrivere così perché lo scopo della dichiarazione di una struct Persona è quello di dire al compilatore quanto è “grande” un valore di questo tipo. Ma se ogni Persona contiene al suo interno altre 2 Persona la sua dimensione sarà infinita.
Per risolvere il problema, usiamo dei puntatori per i genitori; invece di includere direttamente le struct padre e madre, memorizziamo i loro indirizzi (sappiamo che l’indirizzo di una variabile ha una dimensione definita e fissa).
Ecco come si modifica la definizione della struct:
struct Persona {
string nome;
string cognome;
string data_nascita;
Persona *padre; // Puntatore alla struct del padre
Persona *madre; // Puntatore alla struct della madre
};
Ora, possiamo rappresentare i genitori puntando ad altre istanze di Persona.
Ecco l’esempio completo:
#include <iostream>
using namespace std;
struct Persona {
string nome;
string cognome;
string data_nascita;
Persona *padre; // Puntatore al padre
Persona *madre; // Puntatore alla madre
};
int main() {
// Creazione dei genitori
Persona padre = {"Luigi", "Rossi", "15/03/1955", nullptr, nullptr};
Persona madre = {"Anna", "Bianchi", "20/07/1960", nullptr, nullptr};
// Creazione del figlio
Persona figlio = {"Mario", "Rossi", "01/01/1980", &padre, &madre};
// Stampa dei dati del figlio
cout << "Figlio: " << figlio.nome << " " << figlio.cognome << endl;
cout << "Nato il: " << figlio.data_nascita << endl;
// Accesso ai genitori tramite i puntatori
if (figlio.padre) {
cout << "Padre: " << figlio.padre->nome << " " << figlio.padre->cognome << endl;
}
if (figlio.madre) {
cout << "Madre: " << figlio.madre->nome << " " << figlio.madre->cognome << endl;
}
return 0;
}
Cosa succede nel programma:
- Creiamo il padre (
padre) e la madre (madre) come variabili normali. - Creiamo il figlio (
figlio) e usiamo i puntatori&padree&madreper collegarlo ai genitori. - Per accedere ai dati dei genitori tramite i puntatori, usiamo l’operatore
->(es.figlio.padre->nome).
Per accedere ad un attributo di una struct alla quale accediamo da puntatore (come in questo caso per accedere al padre o alla madre di una Persona) si utilizza l’operatore -> al posto dell’operatore ..
Quindi al posto di scrivere il seguente codice:
*(figlio.padre).nome // Mi posiziono dal padre (utilizzando il puntatore) e accedo all'attributo nome
si utilizza il seguente codice:
figlio.padre->nome // Mi posiziono dal padre e accedo all'attributo nome tutto in una operazione