Questo sito utilizza cookie per raccogliere dati statistici.
Privacy Policy
# 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:
```cpp
int numero = 42;
```
il compilatore riserva una zona di memoria per la variabile `numero`. Questa zona è identificata da un indirizzo univoco, che possiamo ottenere utilizzando l'**operatore di indirizzo `&`**:
```cpp
cout << "Indirizzo di numero: " << &numero << endl;
```
Il risultato sarà un valore esadecimale che rappresenta l'indirizzo.
### Indirizzo e dimensione delle variabili
La dimensione di memoria occupata da una variabile dipende dal suo **tipo di dato**. Ad esempio:
- Un `int` occupa generalmente 4 byte.
- Un `char` occupa 1 byte.
- Un `double` occupa 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:
```cpp
int numero = 42;
int *ptr = № // 'ptr' è un puntatore a int
```
Nel codice sopra:
- `ptr` memorizza l'indirizzo di `numero`.
- Il tipo `int *` specifica che `ptr` punta a una variabile di tipo `int`.
### 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:
```cpp
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.
```cpp
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:
```cpp
cout << "Indirizzo di numero: " << &numero << endl;
cout << "Valore di ptr: " << ptr << endl; // Stampa lo stesso indirizzo
```
<ex>
##### 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.
</ex>
---
#### 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.
```cpp
int numero = 42;
int *ptr = №
cout << "Valore puntato da ptr: " << *ptr << endl; // Stampa 42
```
In questo esempio:
- `ptr` contiene l'indirizzo di `numero`.
- Usando `*ptr`, seguiamo l'indirizzo per accedere al valore di `numero`.
La dereferenziazione consente non solo di leggere, ma anche di **modificare** il valore della variabile puntata:
```cpp
*ptr = 10; // Modifica il valore di 'numero' tramite il puntatore
cout << "Nuovo valore di numero: " << numero << endl; // Stampa 10
```
<ex>
##### 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 = ...`).
</ex>
<im>
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.
</im>
---
### Esempio completo
Esempio di Assegnamento e Dereferenziazione
```cpp
#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
1. **Puntatori non inizializzati**: Se un puntatore non è inizializzato (contiene "schifo"), punta a un indirizzo casuale:
```cpp
int *ptr; // Non inizializzato
*ptr = 10; // Errore: accesso a memoria non valida
```
2. **Dereferenziare puntatori nulli**: Un puntatore nullo (`nullptr`) non può essere dereferenziato:
```cpp
int *ptr = nullptr;
cout << *ptr; // Errore: crash dell'applicazione
```
**Best practice**:
- Inizializzare i puntatori a `nullptr` se non si sa subito cosa devono puntare.
- Controllare sempre se un puntatore è nullo prima di dereferenziarlo:
```cpp
if (ptr != nullptr) {
cout << *ptr << endl;
}
```
<ac>
**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.
```cpp
#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:
```cpp
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:
```cpp
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:**
```cpp
#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 `&padre` e `&madre` per collegarlo ai genitori.
- Per accedere ai dati dei genitori tramite i puntatori, usiamo l'operatore `->` (es. `figlio.padre->nome`).
<im>
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:
```cpp
*(figlio.padre).nome // Mi posiziono dal padre (utilizzando il puntatore) e accedo all'attributo nome
```
si utilizza il seguente codice:
```cpp
figlio.padre->nome // Mi posiziono dal padre e accedo all'attributo nome tutto in una operazione
```
</im>
</ac>