Introduzione ai puntatori e ai riferimenti in C++. Viene presentato il problema dello swap, come esempio motivante per l'introduzione della semantica di riferimento. Si procede con l'introduzione del concetto di puntatore, a cui segue una spiegazione dei basilare operatori di referenziazione e dereferenziazione. Il problema dello swap viene risolto mediante puntatori. Si procede con l'introduzione dei riferimenti, come alias di variabili esistenti. Il problema dello swap viene in ultimo risolto mediante riferimenti.
4. Il problema dello swap
¤ Supponiamo che in un programma avvenga
frequentemente lo scambio (swap) tra due interi
4
2 inta: 5 intb:
5. Il problema dello swap
¤ IDEA: Scrivere una funzione int_swap e richiamarla
ovunque necessario
int main() {
...
int a = 2, b = 5;
...
int_swap(a, b); // expected result: a = 5, b = 2
}
5
7. Il problema dello swap
¤ Un possibile tentativo potrebbe essere:
¤ La soluzione sembra immediata…ma funziona?
void int_swap(int x, int y) {
int temp = x;
x = y;
y = temp;
}
7
9. Il problema dello swap
¤ All’invocazione di int_swap:
¤ I valori di a e b nella funzione chiamante vengono copiati
nelle variabili locali x e y
¤ i valori di x e y vengono scambiati, ma…
¤ Le variabili a e b nel chiamante rimangono intoccate
...
int a = 2, b = 5;
int_swap(a, b); // x and y are copies of a and b,
// hence a and b remain unchanged
9
10. Il problema dello swap
¤ Non è possibile scrivere la funzione int_swap passando
le variabili per copia
10
#@?$!
12. Organizzazione della memoria
¤ La memoria può essere immaginata come una
successione di celle di memoria
¤ Ogni cella occupa la minima dimensione che il
calcolatore è in grado di gestire (1 byte*)
*1 byte è il quantitativo più comune
12
15. Organizzazione della memoria
¤ In questo modo:
¤ Ogni cella ha un indirizzo univoco
¤ È nota una regola per passare da una cella alle successive
15
16. Nome di una variabile
¤ Il nome della variabile è lo strumento usato per accedere
al contenuto della memoria riservata a tale variabile
int a = 5;
int b = a + 5; // we use the data contained in ‘a’
// without knowing its physical
// location in memory
16
17. Variabili e memoria
¤ In virtù del fatto che ogni variabile ha un nome, non
dobbiamo solitamente preoccuparci di dove i dati
vengano memorizzati
17
1775 17771776 1778 1779 1780
int a
18. Variabili e memoria
¤ Ciononostante, una variabile occupa una o più celle di
memoria
18
1775 17771776 1778 1779 1780
int a
19. Indirizzo di una variabile
¤ Quando vogliamo riferirci all’indirizzo di una variabile,
piuttosto che il suo valore, utilizziamo la sintassi
&nome_variabile
19
1775 17771776 1778 1779 1780
int a&a
20. Indirizzo di una variabile
¤ L’espressione &a può essere letta come “indirizzo di a”
20
1775 17771776 1778 1779 1780
int a&a
21. Indirizzo di una variabile
¤ Data dunque la definizione:
¤ Si ha che:
¤ a si riferisce al contenuto della variabile (5 in questo caso)
¤ &a si riferisce all’indirizzo dove tale contenuto risiede in
memoria
21
int a = 5;
22. Indirizzo di una variabile
¤ Esempio: stampare a video l’indirizzo di una variabile
22
int a = 5;
std::cout << "the address of a is " << &a
<< std::endl;
23. Indirizzo di una variabile
¤ Quando il simbolo & precede un’espressione, questo
assume il ruolo di operatore (address-of operator)
23
int a = 5;
std::cout << "the address of a is " << &a
<< std::endl;
24. Indirizzo di una variabile
¤ Nota: se una variabile occupa più di una cella di
memoria, l’operatore & restituisce l’indirizzo della prima
cella di memoria utilizzata
1775 17771776 1778 1779 1780
int a&a
24
25. Dimensione dei tipi dati in C++
¤ Non esistono dimensioni prefissate dei tipi dati
¤ La dimensione in byte di ogni tipo dato è dipendente
dall’architettura del calcolatore e dal compilatore
¤ Lo standard C++ specifica unicamente il minimo intervallo di
valori memorizzabili
25
26. Dimensione dei tipi dati in C++
¤ Esempio: un int deve permettere di memorizzare numeri
interi tra -32’767 e +32’767
¤ Tipica dimensione su architettura Intel 16-bit: 2 byte
¤ Tipica dimensione su architettura Intel 32-bit: 4 byte
26
27. Memorizzare un indirizzo
¤ La stampa di un indirizzo non è particolarmente utile
¤ Supponiamo invece di voler memorizzare l’indirizzo di a,
in modo da poterlo riutilizzare successivamente
¤ Abbiamo bisogno di una variabile address_of_a il cui
contenuto sia l’indirizzo di a
int a = 5;
std::cout << "the address of a is " << &a
<< std::endl;
27
31. Memorizzare un indirizzo
¤ Il tipo dato di una variabile che contiene l’indirizzo di un
intero è int*
¤ Il tipo dato int* si chiama puntatore a intero
int* address_of_a = &a;
31
32. Puntatori
¤ Variabili che contengono indirizzi sono chiamate
puntatori
¤ Una variable di tipo T* può contenere l’indirizzo di una
variabile di tipo T
¤ È possibile creare puntatori a qualsiasi tipo dato*,
indipendentemente che questo sia predefinito o creato
dall’utente
Per un tipo dato T, T* è il tipo dato puntatore a T
* Purchè il tipo dato non sia più piccolo di 1 byte
32
33. Esempi di puntatori
¤ Regola (del pollice) per interpretare il tipo dato:
¤ Il più a destra degli asterischi * si legge “puntatore a…”
¤ Tutto ciò che segue da destra a sinistra è il tipo puntato
int* a; // pointer to int
char* b; // pointer to char
float* c; // pointer to float
int** d; // pointer to pointer to int
Per un tipo dato T, T* è il tipo dato puntatore a T
33
35. Accedere al contenuto di una
variabile
¤ Supponiamo che qualcuno ci fornisca un puntatore a
intero ptr contenente l’indirizzo di una variabile intera
35
int* ptr = ... // somehow initialized
36. Accedere al contenuto di una
variabile
¤ Possiamo usare l’indirizzo memorizzato in ptr per
accedere al contenuto della variabile puntata?
36
int* ptr = ... // somehow initialized
37. Indirezione
¤ Dato un puntatore, è possibile accedere al contenuto
della variabile puntata attraverso l’operatore *
(indirection operator)
int* ptr = ... // somehow initialized
std::cout << "the value of the variable
pointed to by ptr is " << *ptr << std::endl;
37
38. Indirezione
¤ L’espressione *ptr può essere letta come
“il valore della variabile puntata da ptr”
int* ptr = ... // somehow initialized
std::cout << "the value of the variable
pointed to by ptr is " << *ptr << std::endl;
38
39. Indirezione
¤ L’operatore di indirezione può essere usato sia per
leggere che per modificare il valore della variabile
puntata
int* ptr = ... // somehow initialized
*ptr = 7;
int b = 5 + *ptr; // b contains 12
39
40. Indirezione
¤ In altre parole, l’espressione *ptr può apparire a sinistra
dell’operatore di assegnamento
40
int* ptr = ... // somehow initialized
*ptr = 7;
int b = 5 + *ptr; // b contains 12
43. Metodi d’accesso
¤ È possibile quindi accedere al contenuto di una variabile
in due modi:
¤ Attraverso il suo nome
¤ Attraverso il suo indirizzo (memorizzato in un puntatore)
¤ L’accesso mediante puntatore è indiretto
¤ per accedere al contenuto della variabile puntata è
necessario prima accedere al contenuto del puntatore
43
44. Puntatori nulli
¤ Come per tutte le variabili locali, non è possibile
conoscere il valore di un puntatore non inizializzato
44
int* ptr; // uninitialized pointer
45. Puntatori nulli
¤ Il puntatore potrebbero contenere un qualsiasi valore
45
int* ptr; // uninitialized pointer
46. Puntatori nulli
¤ Tale valore casuale potrebbe essere interpretato come
l’indirizzo di un ipotetica variabile puntata dal puntatore
46
int* ptr; // uninitialized pointer
47. Puntatori nulli
¤ Non esiste alcun modo di capire se il valore contenuto in
un puntatore rappresenti o meno un indirizzo valido
47
int* ptr; // uninitialized pointer
48. Puntatori nulli
¤ È molto facile trovarsi ad utilizzare un puntatore non
inizializzato senza nemmeno rendersene conto
48
int* ptr; // uninitialized pointer
49. Puntatori nulli
¤ Per evitare il rischio di avere puntatori non inizializzati
sarebbe sufficiente inizializzare un puntatore solo dopo
aver definito la variabile da puntare
49
int a = 5;
int* ptr = &a;
50. Puntatori nulli
¤ Purtroppo, ciò non è sempre realizzabile
¤ in alcuni casi non è effettivamente possibile creare la
variabile da puntare prima dell’inizializzazione del puntatore
¤ Anche senza una variabile da puntare, sarebbe
preferibile evitare rischi inizializzando il puntatore.
50
51. Puntatori nulli
¤ È possibile inizializzare un puntatore e al contempo
indicare che non è ancora disponibile la corrispettiva
variabile a cui puntare
¤ Per farlo si inizializza il puntatore al valore:
¤ NULL (fino al C++03) o
¤ nullptr (dal C++11)
51
52. Puntatori nulli
¤ L’espressione può essere letta come
“il puntatore ptr non punta a nulla”
int* ptr = nullptr; // 'ptr’ is not
// pointing to anything
52
53. Il problema dello swap:
soluzione
¤ Come possiamo usare i puntatori per riuscire a scrivere la
funzione int_swap?
53
54. Il problema dello swap:
soluzione
¤ IDEA: invece di passare una copia del contenuto delle
variabili, passare una copia del loro indirizzo
void int_swap(int* x, int* y) {
int temp = *x;
*x = *y;
*y = temp;
}
54
55. Il problema dello swap:
soluzione
¤ All’invocazione di int_swap:
¤ Gli indirizzi di a e b nella funzione chiamante vengono
copiati nelle variabili puntatore locali x e y
¤ Si scambiano i valori delle variabili puntate da x e y
(ovvero a e b)
¤ Al termine della funzione int_swap le variabili locali x e y
vengono distrutte
¤ Come invocare la nuova funzione int_swap?
55
56. Il problema dello swap:
soluzione
¤ Sappiamo che:
¤ La funzione int_swap si aspetta in ingresso gli indirizzi delle
variabili da scambiare
¤ Per ottenere l’indirizzo di una variabile si usa l’operatore &
56
57. Il problema dello swap:
soluzione
¤ Se vogliamo scambiare il contenuto di a e b dobbiamo
quindi fornire l’indirizzo di a (&a) e l’indirizzo di b (&b)
int main() {
...
int a = 2, b = 5;
...
int_swap(&a, &b); // now: a = 5, b = 2
}
57
58. Passaggio per indirizzo
¤ Se una funzione riceve una copia delll’indirizzo di una
variabile piuttosto che un copia del suo valore, si parla di
passaggio per indirizzo
¤ Il passaggio per indirizzo permette di:
¤ Riflettere le modifiche attutate nella funziona chiamata
anche alla funzione chiamante
¤ Accedere rapidamente a dati corposi, dato che nessuna
copia del dato viene creata
¤ Simulare il ritorno di più variabili da una funzione
58
59. Problema risolto?
¤ Siamo soddisfatti della nostra soluzione?
¤ Il metodo funziona, ma…
¤ Il codice è diventato difficile da leggere
¤ dovremmo gestire il caso di particolare in cui viene passato
nullptr come indirizzo
¤ Possiamo fare di meglio?
int_swap(&a, &b);
59
61. Inizializzare una variabile
¤ Al momento dell’inizializzazione di una variabile, il valore
della variabile inizializzatrice viene copiato nella nuova
variabile
¤ Al termine dell’inizializzazione, ognuna delle due variabili
contiene la propria copia dello stesso valore
¤ La nuova variabile vive una vita indipendente, il
cambiamento del suo valore non altera quello della
variabile inizializzatrice
61
62. Dichiarare una variabile
¤ Quando una variabile viene dichiarata, se ne specifica il
tipo e l’identificativo (nome)
¤ Entrambi questi aspetti non sono modificabili in un
secondo momento
¤ Non è possibile tramutare una variabile di tipo T in una
variabile di tipo T’*
¤ Non è possibile mutare l’identificativo (cambiare nome ad
una variabile)
* ricorda che il casting crea una nuova variabile,
non cambia la natura di una variabile esistente
62
63. Alias di una variabile
¤ Il C++ permette però di inizializzare nomi aggiuntivi (alias)
per una variabile:
int a = 5;
int& b = a; // b is a new name (alias) for a
63
64. Alias di una variabile
¤ b non indica una nuova area di memoria, b è solo un
altro nome per a
int a = 5;
int& b = a; // b is a new name (alias) for a
64
65. Alias di una variabile
¤ b è una variabile di tipo int&, ovvero un alias di una
variabile di tipo int
int a = 5;
int& b = a; // b is a new name (alias) for a
65
66. Riferimenti
¤ Gli alias di una variabile sono chiamati riferimenti
(reference*)
¤ L’inizializzazione di un riferimento non comporta la copia
del valore della variabile inizializzatrice, bensì
l’associazione (binding) del nuovo nome alla variabile
* Dal C++11 sono stati rinominati
lvalue reference
Per un tipo dato T, T& è il tipo dato riferimento a T
66
67. Esempi di riferimenti
¤ Regola (del pollice) per interpretare il tipo dato:
¤ L’ampersand & più a destra si legge “riferimento a…”
¤ Tutto ciò che segue da destra a sinistra è il tipo a cui ci si
riferisce
Per un tipo dato T, T& è il tipo dato riferimento a T
int& a = b; // a is a new name for b (where b is of type int)
float& c = d; // c is a new name for d (where d is of type
float)
int& e = b; // e is a new name for b, hence a new name for a
int*& f = g; // f is a new name for g (where g is of type int*)
67
68. Nuovo vs. vecchio nome
¤ Una volta inizializzato, usare il nuovo nome o il vecchio
nome è indifferente*
int a = 5;
int& b = a; // b is a new name (alias) for a
b = 7;
std::cout << a << std::endl; // the output will be '7'
std::cout << &a
<< &b
<< std::endl; //'a' and 'b' are the same thing,
// so the address is the same
* concetto riassumibile nella massima:
“the reference is the referent”
68
69. Limitazione dei riferimenti
¤ I riferimenti devono essere inizializzati
¤ I riferimenti non possono essere ri-assegnati:
¤ Non sono ammessi* riferimenti a riferimenti
int& b; //ERROR: b is a new name of which variable?
int a = 5, c = 10;
int& b = a;
b = c; // this does not mean that b is now a
// new name for c, the expression amounts to
// say “a = c”
* Dal C++11, esiste un unico strappo alla regola al fine
di permettere il perfect forwarding
69
70. Il problema dello swap:
soluzione
¤ I riferimenti ci permettono di scrivere la funzione
int_swap in maniera semplice e sicura
void swap(int& x, int& y) {
int temp = x;
x = y;
y = temp;
}
Il corpo della
funzione è uguale a
quanto scritto nella
prima (errata)
versione
70
71. Il problema dello swap:
soluzione
¤ I riferimenti ci permettono di scrivere la funzione
int_swap in maniera semplice e sicura:
¤ Semplice: non dobbiamo usare l’operatore di indirezione
ovunque
¤ Sicura: non possiamo per errore passare nullptr
71
72. Il problema dello swap:
soluzione
¤ Per scambiare i valori di a e b, invochiamo la funzione
int_swap esattamente come una qualsiasi altra funzione
72
int main() {
...
int a = 2, b = 5;
...
int_swap(a, b); // now: a = 5, b = 2
}
73. Il problema dello swap:
soluzione
¤ In questo modo:
¤ x è un alias per a
¤ y è un alias per b
int main() {
...
int a = 2, b = 5;
...
int_swap(a, b); // now: a = 5, b = 2
}
73
74. Il problema dello swap:
soluzione
¤ Modificare x e y è la stessa cosa che modificare a e b
74
int main() {
...
int a = 2, b = 5;
...
int_swap(a, b); // now: a = 5, b = 2
}
76. Bibliografia
¤ S. B. Lippman, J. Lajoie, B. E. Moo, C++ Primer (5th Ed.)
¤ B. Stroustrup, The C++ Programming Language (4th Ed.)
¤ R. Lafore, Object Oriented Programming in C++ (4th Ed.)
¤ C++FAQ, Section 8
http://www.parashift.com/c++-faq/references.html
76