Tutorato di Programmazione A.A. 2016-2017

Puntatori

Antonino Furnari http://www.dmi.unict.it/~furnari/ - furnari@dmi.unict.it

Convenzioni

Lo scopo di questa esercitazione è guidare il lettore attraverso la comprensione di concetti chiave e la loro implementazione pratica. Per facilitare la comprensione degli argomenti proposti, durante l'esercitazione, viene richiesto al lettore di rispondere ad alcune domande e risolvere alcuni esercizi pratici. Tali richieste sono indicate dai seguenti simboli:

Questo simbolo è presente laddove viene richiesto al lettore di rispondere a una domanda.
Questo simbolo è presente laddove viene richiesto di inserire la risposta a una domanda.
Questo simbolo è presente laddove viene richiesto a lettore di svolgere un esercizio.

1. Operatore di referenziazione &

Le variabili in C/C++ sono delle locazioni di memoria alle quali è possibile accedere mediante un identificatore (il nome della variabile). In questo modo, per il programmatore non è necessario preoccuparsi di gestire esplicitamente gli indirizzi fisici che identificano le porzioni di memoria in cui i dati risiedono.

Per un programma C/C++, la memoria resta una serie di locazioni che vengono assegnate e gestite in successione. Ad esempio, quando si definiscono due variabili intere in successione, ad esse vengono assegnati due indirizzi fisici consecutivi. L'indirizzo di una variabile può essere ottenuto mediante l'operatore di referenziazione &:

In [1]:
#include <iostream>
using namespace std;

int a = 5;
int b = 18;

cout << &a << endl;
cout << &b << endl;
0x7f67d32a9014
0x7f67d32a9018
Out[1]:
(std::basic_ostream<char, std::char_traits<char> >::__ostream_type &) @0x7f67caea9700

Domanda 1.1

Gli indirizzi ottenuti sono consecutivi? Qual è la distanza tra i due indirizzi? Perché? Provare a ripetere l'esercizio definendo due variabili di tipo double. Qual è il risultato?

Risposta 1.1






I simboli a e &a rappresentano due cose diverse:

  • a è una variabile contenente l'intero $5$;
  • &a contiene l'indirizzo di a.

Ciò è facilmente verificabile come segue:

In [2]:
cout << a << endl;
cout << &a << endl;
5
0x7f67d32a9014
Out[2]:
(std::basic_ostream<char, std::char_traits<char> >::__ostream_type &) @0x7f67caea9700

I due simboli hanno due tipi diversi. Nello specifico:

  • a è di tipo intero;
  • &a è di tipo puntatore a intero.

Se tipo è un determinato tipo di dato, il tipo di dato "puntatore a tipo" si indica con tipo *. E' dunque possibile definire una variabile di tipo "puntatore a intero" come segue:

In [3]:
int *ptr;
cout << ptr << endl;
0
Out[3]:
(std::basic_ostream<char, std::char_traits<char> >::__ostream_type &) @0x7f67caea9700

Quando dichiariamo un puntatore senza assegnargli alcun valore, questo viene inizializzato a zero di default. L'indirizzo "0" non è un indirizzo valido e viene spesso indicato come null. Quando un puntatore ha valore zero possiamo assumere che esso "non punta a nulla".

Adesso ptr è una normale variabile di tipo "puntatore a intero" e dunque possiamo assegnare ad essa un qualsiasi valore del suo stesso tipo. Ad esempio:

In [4]:
ptr = &a;
cout << &a << endl;
cout << ptr << endl;
0x7f67d32a9014
0x7f67d32a9014
Out[4]:
(std::basic_ostream<char, std::char_traits<char> >::__ostream_type &) @0x7f67caea9700

Domanda 1.2

E' possibile assegnare un intero alla variabile ptr mediante un assegmento del tipo "ptr=5;"? Perché?

Risposta 1.2






Domanda 1.3

Qual è il risultato del seguente codice?

int x = 12;
double y = 52;

int *ptr1 = &x;
int *ptr2 = &y;

Come è possibile modificare il codice per evitare eventuali problemi?

Risposta 1.3






2. Operatore di dereferenziazione *

Un puntatore p è una variabile che contiene l'indirizzo di memoria di un'altra variabile x. Pertanto, in genere si dice che p punta a x. I puntatori possono essere utilizzati per accedere al contenuto della memoria che referenziano in maniera diretta. Questo può essere fatto mediante l'operatore di dereferenziazione * come segue:

In [5]:
int var1 = 228;
int *p1;
p1 = &var1;

cout << *p1 << endl;
228
Out[5]:
(std::basic_ostream<char, std::char_traits<char> >::__ostream_type &) @0x7f67caea9700

In pratica, anteponendo l'operatore "" alla nome della variabile puntatore. E' possibile accedere al suo valore, sia in lettura che in scrittura. Va notato che, analogamente a quanto avviene per &var1 e p1, var1 e `p1` sono in pratica equivalenti:

In [6]:
var1 = 522;
cout << *p1 << endl;

*p1 = 722;
cout << var1 << endl;

cout << &var1 << endl;
cout << p1 << endl;
522
722
0x7f67d32a9030
0x7f67d32a9030
Out[6]:
(std::basic_ostream<char, std::char_traits<char> >::__ostream_type &) @0x7f67caea9700

*p1 è un intero. E' pertanto possibile assegnare il suo valore a altre variabili intere:

In [7]:
int var2;
var2 = *p1;
Out[7]:
(int) 722
In [8]:
*p1=5;
cout << var2;
722
Out[8]:
(std::basic_ostream<char, std::char_traits<char> >::__ostream_type &) @0x7f67caea9700

Domanda 2.1

Cosa stampa a schermo il seguente codice? Rispondere senza l'uso del computer prima, poi implementare per verificare il risultato. Spiegare il perché del risultato.

int x = 18;
int *h = &x;
x++;

int y = *h;
y++;

cout << y << endl;
cout << x << endl;

Risposta 2.1






Domanda 2.2

Qual è il risultato del seguente codice?

double d = 3.14;
double *ptrd = &d;

double c;
double *ptrc = &c;

*ptrc = d;
c-=0.14;

cout << d << endl;
cout << c << endl;

*ptrd = *ptrc/2;

ptrd = ptrc;

*ptrd/=2;

cout << d;

Provare a rispondere prima e poi implementare il codice per verificare la correttezza della risposta.

Risposta 2.2






3. Puntatori come parametri di funzioni e passaggio per riferimento

Un possibile uso dei puntatori è quello relativo al passaggio per riferimento di parametri a una funzione. Se non diversamente specificato, in C/C++ i parametri vengono passati alle funzioni per valore. Ciò significa che il valore del parametro viene copiato nello stack della funzione chiamata. Quando l'esecuzione della funzione termina, qualsiasi modifica effettuata all'interno dello stack viene persa.

Domanda 3.1

Si consideri la seguente funzione:

int sum(int a, int b) {
    a+=b;
    return a;
}

Si consideri dunque il seguente codice:

int v1 = 15;
int v2 = sum(v1,7);
  • Dove risiede la variabile a? (heap o stack?)
  • Quali sono i valori di a e b prima dell'esecuzione della istruzione di return?
  • Quali sono i valori di v1 e v2 alla fine dell'esecuzione del codice?

Risposta 3.1






I puntatori possono essere utilizzati per permettere alla funzione di accedere in scrittura a una porzione di memoria definita fuori dalla funzione. Si consideri ad esempio il seguente codice:

int sum(int* ptr_a, int b) {
    (*ptr_a)+=b;
    return *ptr_a;
}

int x = 10;
int y = 15;

int z = sum(&x,y);

In questo caso, la funzione sum può accedere in scrittura alla variabile x definita al di fuori della funzione in quanto essa opera sul suo puntatore.

Domanda 3.2

Nella funzione sum:

int sum(int* ptr_a, int b);

il passaggio del puntatore ptr_a viene effettuato per valore o per riferimento?

All fine del codice riportato sopra:

int x = 10;
int y = 15;

int z = sum(&x,y);

quali sono i valori di x, y e z?

Risposta 3.2






Un uso interessante dei puntatori come parametri delle funzioni, è quello di permettere a una funzione di restituire più di un valore in output. In generale infatti, una funzione permette di restituire solo un valore di output, ad esempio:

int sum(int a, int b) {
    return a+b;
}

La funzione definita sopra prende in input due interi e ne restituisce uno. Supponiamo adesso di dover definire una funzione che prende in input due numeri e ne restituisca somma e prodotto. Ciò può essere ottenuto come segue:

void sum_prod(int a, int b, int *sum, int *prod) { //gli ultimi due parametri sono dei puntatori ai valori di output
    *sum = a+b;
    *prod = a*b;
}

La funzione può dunque essere utilizzata come segue:

int sum, prod;

sum_prod(3,5,&sum,&prod);

Alla fine del codice riportato sopra, le variabili sum e prod conterranno la somma e il prodotto dei due input.

Esercizio 3.1

Si scriva una funzione che prenda in input due parametri e ne restituisca minimo e massimo.

3.1 Passaggio di parametri per riferimento

In genere, è possibile passare un parametro direttamente per riferimento, senza dover esplicitamente gestire i puntatori. Ciò viene fatto anteponendo il simbolo & al nome del parametro che vogliamo passare per riferimento nella definizione della funzione:

void increment(int &a) {
    a++;
}

Alla fine del seguente codice:

int x =0;
increment(x);

il valore di x sarà 1.

L'equivalente della funzione increment facendo uso esplicito dei puntatori è la seguente:

void increment_ptr(int *a) {
    (*a)++;
}

In questo caso però, la funzione va usata come segue:

int y = 0;
increment_ptr(&y);

Domanda 3.3

Si considerino le funzioni increment e increment_ptr. Quale delle due è definita in maniera "più naturale"? Quale in maniera "più esplicita"? Quali sono i vantaggi dell'una e dell'altra definizione? In quali dei due casi i parametri vengono passati per riferimento? In quale dei due casi per valore?

Risposta 3.3






Esercizio 3.2

Si riscriva la funzione sviluppata all'esercizio 3.1 sostituendo l'uso esplicito dei puntatori con il passaggio per riferimento mediante simbolo &.

4. Puntatori di sola lettura e puntatori costanti

Abbiamo visto che una data variabile e un puntatore ad essa sono in genere "intercambiabili". E' cioè possibile scrivere e leggere il contenuto della cella di memoria relativa utilizzando direttamente la variabile o dereferenziandone il puntatore.

In alcuni casi tuttavia può essere utile definire dei puntatori di "sola lettura". Si tratta di puntatori attraverso i quali è possibile leggere il contenuto della cella di memoria relativa ma non scriverci sopra. Questo tipo di puntatore può essere utile quando abbiamo bisogno di passarlo come parametro di una funzione e vogliamo assicurarci che la funzione acceda ai dati in sola lettura.

Per dichiarare un puntatore di sola lettura, dobbiamo opportunamente utilizzare la parola chiave const:

In [9]:
int var3 = 118;
const int* ptr3 = &var3;

cout << *ptr3 << endl; //accesso in lettura consentito
//*ptr3=18; //questa istruzione genererebbe un errore
118
Out[9]:
(std::basic_ostream<char, std::char_traits<char> >::__ostream_type &) @0x7f67caea9700

Va precisato che la proprietà di "sola lettura" appartiene al puntatore *ptr3 e non alla variabile var3 che potrà comunque essere modificata direttamente:

In [10]:
var3--;

cout << *ptr3 << endl;
//(*ptr3)--; //questa istruzione genererebbe un errore
117
Out[10]:
(std::basic_ostream<char, std::char_traits<char> >::__ostream_type &) @0x7f67caea9700

Domanda 4.1

Si consideri la funzione dichiarata come segue:

double quadrato(double* x); //calcola il quadrato di x

Si consideri inoltre il seguente codice:

double x = 2.2;
double y = quadrato(&x);

Possiamo assumere con certezza che x=2.2 e y=4,84? Come potremmo modificare la dichiarazione della funzione per avere qualche certezza in più?

Risposta 4.1






Esercizio 4.1

Si definisca una funzione stats che prenda in input 5 puntatori a interi in1, in2, out_min, out_max, out_mean. La funzione deve calcolare il minimo, massimo e la media dei due valori in input e inserire i risultati nei due parametri di output.

Utilizzare il modificatore const per specificare chiaramente quali parametri verranno utilizzati solo in lettura e quali anche in scrittura.

Si noti che è possibile utilizzare il modificatore const in maniera simile quando si passano dei parametri per riferimento. Ad esempio:

void sum(int const &x, int const &y, int &out) {
    out = x+y;
    x++; //questa istruzione produce un errore di compilazione. x è di sola lettura!
}

Esercizio 4.2

Riscrivere la funzione definita nell'esercizio 4.1 sostituendo l'uso esplicito dei puntatori con il passaggio per riferimento mediante simbolo &. Anche in questo caso si utilizzi il modificatore const in maniera opportuna.

Domanda 4.2

Qual è il risultato del seguente codice? Motivare la risposta.

void fun(int x) {
    int *ptr = &x;
    int y = 5;
    *ptr = y;
}

int val=10;
fun(val);
cout << val;

Risposta 4.2






4.1 Puntatori costanti

La keyword const può essere utilizzata anche per definire dei puntatori costanti. Un puntatore costante si comporta in maniera simile a una variabile costante: contestualmente alla sua definizione, è possibile assegnare un solo valore (ovvero un solo indirizzo) che non è più possibile cambiare in futuro. E' possibile dichiarare un puntatore costante come segue:

In [11]:
int var = 12;
int* const const_ptr = &var;
Out[11]:
(int *const) 0x7f67d32a9050

Vediamo qualche esempio:

int var;
var = 15; //assegnamento possibile, var non è costante

const int const_var = 10; //variabile costante di tipo intero
const_var = 0; //questo assegnamento non è possibile -> errore di compilazione!

int* const const_ptr = &var; //puntatore costante
const_ptr = &var; //questo assegnamento non è possibile -> errore di compilazione!
(*const_ptr)++; //questo incremento è possibile: const_ptr non è di sola lettura

const int* read_only_ptr = &var; //puntatore di sola lettura
read_only_ptr = &var; //questo assegnamento è possibile, read_only_ptr non è un puntatore costante

*(read_only_ptr)++; //incremento non possibile, il puntatore è di sola lettura

SUGGERIMENTO SULLE NOTAZIONI

Le differenze tra la dichiarazione di un puntatore di sola lettura e un puntatore costante sono molto sottili. Per distinguere le due dichiarazioni, è possibile seguire questo schema:

const int* p;

dichiara un puntatore di sola lettura. La dichiarazione può essere vista come

(const int)* p; //attenzione, si tratta solo di uno "schema", questa istruzione dà errore di compilazione!

che si legge "p è di tipo puntatore a intero costante", da cui deriva che l'intero a cui punta p non può essere modificato mediante p.

Si noti che le dichiarazioni:

const int* p; //(const int)* p;
int const* p; //(int const)* p;

sono del tutto equivalenti: in entrambi i casi p è un puntatore di sola lettura a intero. Si consiglia però di utilizzare la prima notazione, che risulta più leggibile.

La notazione

int* const p;

dichiara un puntatore costante. La dichiarazione può essere vista come:

(int)* const p; //attenzione, si tratta solo di uno "schema", questa istruzione dà errore di compilazione!

che si legge "p è una costante di tipo puntatore a intero".

Domanda 4.3

Quali sono i vantaggi di un puntatore costante? Che tipo di "garanzie" vengono offerte dai puntatori costanti? Ad esempio, dato il codice:

int var = 18;
int* const p = &var;

posso affermare che (*p)==var in un punto indefinito del programma successivo al codice visto sopra? Perché?

Risposta 4.3






5 Puntatori e array

Puntatori e array sono due concetti molto correlati. Un array infatti si comporta in maniera molto simile a un puntatore al suo primo elemento. Si considerino ad esempio le seguenti dichiarazioni:

In [12]:
int arr[5] = {1,2,3,4,5};
int *ptra;
Out[12]:
(int *) nullptr

Il seguente assegnamento è perfettamente valido:

In [13]:
ptra = arr;
Out[13]:
(int *) 0x7f67d32a9060

Inoltre, il simbolo arr si comporta proprio come un puntatore al primo valore dell'array:

In [14]:
cout << ptra << endl;
cout << &arr[0] << endl;
cout << arr << endl << endl;

cout << *ptra << endl;
cout << arr[0] << endl;
cout << *arr << endl << endl;

cout << arr[0]<<endl;
*ptra=25;
cout << arr[0];
0x7f67d32a9060
0x7f67d32a9060
0x7f67d32a9060

1
1
1

1
25
Out[14]:
(std::basic_ostream<char, std::char_traits<char> >::__ostream_type &) @0x7f67caea9700

In pratica, un array può essere visto come un puntatore costante al primo elemento. Dato che il puntatore è costante, alcune operazioni non sono permesse:

int val = 18;
arr = &val; //operazione non permessa: non si può modificare l'indirizzo di arr

Domanda 5.1

Si consideri il codice:

void transform(int &x, int &y) {
    x = y*y;
    y = 5;
}

int arr[2] = {1,2};
int *pp1 = arr;
int *pp2 = &arr[1];

transform(*pp1,*pp2);

che valori sono contenuti in arr alla fine dell'esecuzione del codice? Motivare la risposta.

Risposta 5.1






Domanda 5.2

Si consideri il codice:

void transform_array(int a[]) {
    a[0]=a[1];
    a[1]=-1;
}

int arr[2] = {1,2};
transform_array(arr);

che valori sono contenuti in arr alla fine dell'esecuzione del codice? Motivare la risposta.

Risposta 5.2






Domanda 5.3

Si considerino le funzioni:

void transform_array1(int *a) {
    *a = -1;
}

void transform_array2(int *a) {
    a[0]=a[1];
    a[1]=-1;
}

Data la dichiarazione:

int arr[2] = {1,2};

1) il seguente codice è corretto?

transform_array1(arr);

2) e il seguente?

transform_array2(arr);

Motivare la risposte.

Risposta 5.3






6 Aritmetica dei puntatori

C/C++ permette di eseguire operazioni aritmetiche sui puntatori. Le uniche operazioni permesse sono somme e sottrazioni con numeri interi. In pratica, dato un puntatore a un determinato tipo di dato (es puntatore a float), incrementare di una unità un puntatore permette di far avanzare l'indirizzo di memoria puntato dal puntatore di un numero di byte pari alla dimensione del tipo di dato considerato. Vediamo un esempio pratico:

In [15]:
int v = 0;
int *pt1 = &v;

cout << pt1 << endl;
pt1=pt1+1;
cout << pt1 << endl;
pt1+=1;
cout << pt1 << endl;
pt1++;
0x7f67d32a9098
0x7f67d32a909c
0x7f67d32a90a0
Out[15]:
(int *) 0x7f67d32a90a0

Ogni volta che incrementiamo il puntatore pt1, il suo indirizzo avanza di $4$ byte (la dimensione di un intero in memoria). Analogamente:

In [16]:
cout << pt1 << "   +4   " << pt1+4 << endl; //fa avanzare l'indirizzo di 4*4=16 bytes
cout << pt1 << "   -1   " << pt1-1 << endl; //fa retrocedere l'indirizzo di 4 bytes
0x7f67d32a90a4   +4   0x7f67d32a90b4
0x7f67d32a90a4   -1   0x7f67d32a90a0
Out[16]:
(std::basic_ostream<char, std::char_traits<char> >::__ostream_type &) @0x7f67caea9700

Domanda 6.1

Si considerino le variabili:

char a ='c';
short b = 2;
long c = 3;
char *ptra = &a;
short *ptrb = &b;
long *ptrc = &c;

Si supponga che gli indirizzi delle variabili a, b e c siano i seguenti:

  • a: 1772;
  • b: 4862;
  • c: 9957;

Si supponga inoltre che, nel sistema in uso, la dimensione di un char sia 1 byte, quella di uno short sia 2 byte e quella di un long sia 8 byte.

Quali sarebbero gli indirizzi dei seguenti puntatori?

  • ptra+8
  • ptrb-2
  • ptrc+3

Risposta 6.1






6.1 Aritmetica dei puntatori e array

Abbiamo visto che un array è un puntatore costante al primo elemento dell'array stesso. Mediante le regole di base dell'aritmetica dei puntatori è dunque possibile manipolare gli array:

In [17]:
int myarray[5] = {2,8,5,1,-1};
cout << *myarray << endl; //equivalente a myarray[0] (2)
cout << myarray[0] << endl << endl; // (2)

cout << *(myarray+2) << endl; //equivalente a myarray[2] (5)
cout << myarray[2] << endl << endl; // (5)

cout << *(myarray+4) << endl; //equivalente a myarray[4] (-1)
cout << myarray[4] << endl << endl; // (-1)
2
2

5
5

-1
-1

Out[17]:
(std::basic_ostream<char, std::char_traits<char> >::__ostream_type &) @0x7f67caea9700

Un array è un puntatore costante, per cui non è possibile modificarne il valore con operazioni del tipo:

myarray++;

Tuttavia, queste operazioni sono possibili su un puntatore sul quale possiamo copiare l'indirizzo di myarray:

In [18]:
int *p_array = myarray;
cout << *p_array <<endl; //equivalente a myarray[0] (2)
cout << myarray[0] <<endl << endl; //(2)

p_array++; //incrementa il puntatore di una locazione. Adesso punta a myarray[1]
cout << *p_array <<endl; //equivalente a myarray[1] (8)
cout << myarray[1] <<endl; //(8)
2
2

8
8
Out[18]:
(std::basic_ostream<char, std::char_traits<char> >::__ostream_type &) @0x7f67caea9700

E' dunque possibile accedere agli elementi di un array come segue:

In [19]:
int *temp_p=myarray;
for (int i=0; i<5; i++) {
    cout << "array[" <<i << "] ->" << *temp_p << endl;
    temp_p++;
}
array[0] ->2
array[1] ->8
array[2] ->5
array[3] ->1
array[4] ->-1
Out[19]:

Esercizio 6.1

Si dichiari un array array di double contenente i seguenti 5 elementi: $[2.0, 4.2, 9.6, 9.2, 0.1]$. Facendo uso della aritmetica dei puntatori:

  • Si cambi il valore del terzo elemento in $9.9$;
  • Si sostituisca il valore del primo elemento nella somma dei valori dell'ultimo e del quarto elemento;
  • Si mostrino a schermo tutti i valori dell'array in sequenza inversa.

Domanda 6.2

Si consideri il codice:

int arr[5] = {1,2,3,4,5};
int *ptr = arr;

Cosa viene stampato a schermo dalle seguenti istruzioni?

  • cout << ptr;
  • cout << ptr+8;
  • cout << *(ptr+2)
  • cout << ptr[2]
  • cout << *(ptr++)
  • cout << *(++ptr)

Cosa fanno le seguenti istruzioni?

  • ++ptr
  • ++(*ptr)
  • *(ptr++)

Risposta 6.2






Domanda 6.3

Si consideri il seguente codice:

char str[] = "hello";

Cosa stampano a schermo le seguenti istruzioni?

  • cout << str[2];
  • cout << *str;
  • cout << *(str+3);
  • cout << (char)(*str+1);

Risposta 6.3






7 Puntatori a puntatori, puntatori a funzioni e puntatori a strutture

Abbiamo visto come i puntatori possano puntare a diversi tipi di dato, quali int e double. In pratica, un puntatore può anche puntare a un altro puntatore. Per definire un puntatore a puntatore, bisogna utilizzare due asterischi:

In [20]:
int i1 = 15; //variabile int
int * pi1 = &i1; //pi1 è un puntatore a i1
int ** ppi1 = &pi1; //ppi1 è un puntatore a pi1, che a sua volta punta a i1

cout << i1 << endl; //valore di i1
cout << &i1 << endl; //indirizzo di i1
cout << pi1 << endl; //valore di pi1 -> indirizzo di i1
cout << &pi1 << endl; //indirizzo di pi1
cout << *pi1 << endl; //deferenziazione di pi1 -> valore di i1
cout << ppi1 << endl; //valore di ppi1 -> indirizzo di pi1
cout << *ppi1 << endl; //deferenziazione di ppi1 -> valore di pi1 -> indirizzo di i1
cout << **ppi1 << endl; //doppia deferenziazione di ppi1 -> valore di i1
15
0x7f67d32a90d0
0x7f67d32a90d0
0x7f67d32a90d8
15
0x7f67d32a90d8
0x7f67d32a90d0
15
Out[20]:
(std::basic_ostream<char, std::char_traits<char> >::__ostream_type &) @0x7f67caea9700

Domanda 7.1

Si consideri il seguente codice:

int var = 19;
int *ptr = &var;
int **ptr2 = &ptr;

Di che tipo sono i seguenti simboli?

  • var;
  • &var;
  • ptr;
  • *ptr;
  • ptr2;
  • *ptr2;
  • **ptr2;

Risposta 7.1






7.2 Puntatori a funzioni

C++ permette di definire puntatori a funzioni. Questa pratica risulta utile nei casi in cui sia necessario passare una funzione come parametro a un'altra funzione e, in generale, quando risulta necessario definire quale funzione utilizzare a run time piuttosto che a design time.

I puntatori a funzione sono definiti in maniera simile a una dichiarazione di funzione ma, in questo caso, il nome della funzione è preceduto da un asterisco e racchiuso tra parentesi tonde. Vediamo un esempio:

int minfun(int a, int b) {
    if(a<b) {
        return a;
    } else {
        return b;
    }
}

int main(int argx, char ** argv) {
    int (*myfun)(int,int) = myfun; //definisco un puntatore a una funzione che
    //prende in input due int e restituisce due int e lo faccio puntare alla funzione minfun

    cout << (*myfun)(3,8); //invocazione della funzione
}

Esercizio 7.1

Definire:

  • una funzione sumfun che restituisca la somma tra due interi;
  • una funzione prodfun che restitusica il prodotto tra due interi;
  • un intero x=5;
  • un intero y=8;

Si definisca dunque un puntatore ptr a funzioni che prendono due interi e restituiscono un intero.

Si scriva un controllo di tipo if che:

  • se x<y faccia puntare ptr a sumfun;
  • faccia puntare ptr a prodfun altrimenti;

Si esegua la funzione a cui punta ptr usando x e y come input e si stampi il valore restituito a schermo.

I puntatori a funzione sono utili quando vogliamo passare una funzione come puntatore a un'altra funzione:

int dosomething(int a, int b, int (*fun)(int,int)) {
    a=a+b;
    return (*fun)(a,b);
}

int minfun(int a, int b) {
    if(a<b) {
        return a;
    } else {
        return b;
    }
}

int maxfun(int a, int b) {
    if(a>b) {
        return a;
    } else {
        return b;
    }
}

int main(int argx, char ** argv) {
    int x = 3;
    int y = 8;

    int (*mi)(int,int)=minfun; //puntatore a minfun
    int (*ma)(int,int)=maxfun; //puntatore a maxfun

    cout << dosomething(x,y,mi) << endl; //"dosomething" con minfun
    cout << dosomething(x,y,ma) << endl; //"dosomething" con maxfun
    cout << dosomething(x,y,&maxfun) << endl; //stesso risultato della riga precedente
}

Esercizio 7.2

Definire (riutilizzare il codice scritto per l'esercizio 7.1):

  • una funzione sumfun che restituisca la somma tra due interi;
  • una funzione prodfun che restitusica il prodotto tra due interi;
  • un intero x=5;
  • un intero y=8;

Si definisca una funzione myfun che prenda in input due interi e un puntatore ptr a funzioni che prendono due interi e restituiscono un intero.

Si scriva un controllo di tipo if che:

  • se x<y stampi a schermo il valore restituito da myfun quando vengono passati come parametri:
    • x;
    • y;
    • un puntatore a prodfun;
  • se x>=y stampi a schermo il valore restituito da myfun quando vengono passati come parametri:
    • x;
    • y;
    • l'indirizzo di sumfun;

7.3 Puntatori a struct

E' inoltre possibile definire puntatori a struct. Ciò può essere utile quando si vuole permettere a una funzione di modificare lo stato interno di una struct.

Domanda 7.2

Si consideri il seguente codice:

struct point3d {
    int x;
    int y;
    int z;
};

void scalePoint(point3d p,int scale) {
    p.x*=scale;
    p.y*=scale;
    p.z*=scale;
}

Cosa stampa a schermo il seguente codice?

int main(int argx, char ** argv) {
    point3d myp;
    myp.x=2;
    myp.y=8;
    myp.z=12;

    scalePoint(myp,2);

    cout << myp.x;
}

Risposta 7.2






Un puntatore a una struct si definisce molto semplicemente come segue:

point3d p;
p.x=2;
p.y=12;
p.z=18;

point3d *pp=&p;

cout << p.x << " = " << (*pp).x<<endl; //stesso risultato con accesso diretto
cout << p.y << " = " << (*pp).y<<endl; //e dereferenziazine del puntatore a struct
cout << p.z << " = " << (*pp).z<<endl;

Esercizio 7.3

Si riscriva il codice contenuto nella Domanda 7.2 in modo che scalePoint possa modificare lo stato interno della struct. Fare uso dei puntatori a struct.