Capitolo 7
Puntatori
I puntatori sono senza dubbio una delle caratteristiche più importanti ed essenziali del linguaggio C. La corretta e adeguata comprensione del loro utilizzo è imprescindibile per padroneggiare proficuamente il linguaggio.
Essi forniscono sia un potente meccanismo per scrivere programmi veloci, efficienti e compatti sia un utile strumento attraverso il quale fornire un accesso diretto alla memoria, allocare e deallocare dinamicamente la memoria, costruire complesse strutture di dati (liste collegate, pile, code, alberi, grafi e così via), passare alle funzioni dei riferimenti a tipi di dato derivati e complessi (per esempio i tipi struttura) evitando l’overhead del copia dei rispettivi membri, modificare direttamente all’interno di una funzione chiamata una variabile passata come argomento dalla rispettiva funzione chiamante, fornire una “sintassi” alternativa di accesso e manipolazione degli elementi di un array e così via per altre soluzioni a problemi algoritmici o computazionali di vario tipo.
Dal punto di vista semantico, per dirla in modo semplice, un puntatore altro non è che una variabile specializzata a contenere un indirizzo di memoria di un’altra variabile.
Ricordiamo, infatti, che ogni variabile di un programma occupa
una determinata quantità di memoria a seconda del suo tipo (per
esempio, in un sistema a 32 bit una variabile di tipo
int
può occupare 4 byte) ed è localizzabile
precisamente attraverso un indirizzo che è esso stesso un valore
numerico (per esempio, la predetta variabile di tipo
int
potrebbe occupare gli indirizzi di memoria, byte
per byte, 0x0109f9f4
, 0x0109f9f5
,
0x0109f9f6
e 0x0109f9f7
e l’indirizzo
0x0109f9f4
sarebbe il suo indirizzo a partire dal
quale localizzarla).
NOTA
Il Capitolo 1, al paragrafo “La memoria centrale”, contiene un’analisi dettagliata su come è organizzata e rappresentata la memoria di un elaboratore.
In modo più rigoroso e formale, un puntatore è definibile come
un tipo derivabile da un tipo funzione oppure da un
qualsiasi tipo oggetto (per esempio una variabile) denominato tipo
referenziato. In pratica rappresenta un oggetto contenente
un valore che è un riferimento, un puntamento, verso un altro
oggetto che è, per l’appunto, il tipo referenziato. Possiamo dunque
asserire che un puntatore derivato da un tipo referenziato
T
è definibile come un puntatore a
T
.
Ritornando alla nostra variabile di tipo int
di 32
bit contenente il valore 10
, potremmo definire un
puntatore verso di essa, ossia potremmo costruire un tipo derivato
(un puntatore a un int
) che è in grado di contenere
l’indirizzo di memoria dove è localizzabile la predetta variabile
e, per il tramite di esso, compiere su quest’ultima delle
operazioni di lettura e/o di scrittura.
La Figura 7.2 mostra il consueto modo, usato in letteratura, per indicare che un oggetto puntatore punta a un determinato oggetto: in pratica, la variabile puntatore ha come contenuto una freccia che indica il puntamento verso l’oggetto referenziato (evidentemente la medesima freccia può essere sostituita con l’indirizzo in memoria della variabile puntata).
NOTA
Un puntatore, essendo esso stesso un oggetto, ha un proprio indirizzo in memoria che è differente dall’indirizzo in memoria in esso contenuto che appartiene all’oggetto referenziato.
Sintassi di base dei puntatori
Un puntatore, come detto, è una variabile che contiene come
valore un indirizzo di memoria appartenente a un oggetto di un
determinato tipo. Per dichiararla come tale bisogna usare una
sintassi particolare che prevede il consueto specificatore di tipo
e identificatore, ma con in più il carattere asterisco
(*
) posto tra lo specificatore e l’identificatore
(Sintassi 7.1).
Sintassi 7.1 Dichiarazione di un puntatore.
data_type *ptr_identifier;
Così lo Snippet 7.1 dichiara la variabile data
come
un puntatore a un tipo int
, ossia stabilisce che
data
potrà contenere un riferimento verso qualsiasi
oggetto di tipo intero (potrà contenerne l’indirizzo di
memoria).
Snippet 7.1 Dichiarazione di un puntatore a un int.
int *data;
Se si esegue la dichiarazione di cui lo Snippet 7.1, il
compilatore predisporrà dello spazio in memoria dove sarà allocato
un puntatore a un int
che, però, inizialmente potrà
essere non inizializzato con un valore congruo o corretto,
cioè potrà contenere un “qualsiasi” indirizzo di memoria, in
generale, non validamente referenziabile (Figura 7.3).
In linea generale la quantità di spazio di allocazione per un puntatore è dipendente dal sistema in uso; potrà essere, per esempio, di 4 byte su un sistema a 32 bit oppure di 8 byte su un sistema di 64 bit.
SUGGERIMENTO
Se desideriamo scoprire quanto spazio di memoria
il compilatore allocherà per un puntatore sul sistema in uso,
possiamo utilizzare l’operatore sizeof
. Per esempio,
sull’attuale sistema di compilazione, l’istruzione sizeof
data
ritornerà come valore 4, ossia il compilatore impegnerà
4 byte di memoria (32 bit) per memorizzare un determinato indirizzo
di memoria.
Dopo la dichiarazione di un puntatore il passo successivo è quello di assegnargli come valore un indirizzo di memoria di un altro oggetto compatibile (Sintassi 7.2).
Sintassi 7.2 Assegnamento di un indirizzo di un oggetto a un puntatore.
ptr_identifier = &object;
In pratica è sufficiente adoperare l’operatore di indirizzamento
(o indirizzo di) espresso mediante il simbolo e
commerciale (&
) sull’oggetto desiderato.
Lo Snippet 7.2 dichiara una variabile di tipo int
denominata value
e poi assegna l’indirizzo di memoria
dove è localizzata alla variabile di tipo puntatore a
int
denominata data
.
Snippet 7.2 Assegnamento di un indirizzo di una variabile int a un puntatore a un int.
int value = 10;
int *data = &value; // data conterrà l'indirizzo di value
Dalla Figura 7.4 si evince come dopo l’assegnamento
dell’indirizzo di memoria di value
il puntatore
data
punti alla variabile value
medesima
perché, ripetiamo, tale puntatore contiene come valore
quell’indirizzo.
Infine, il seguente operatore, detto di
indirezione o deriferimento, espresso mediante il
simbolo asterisco *
e prefisso all’identificatore di
un puntatore, consente di accedere al contenuto di un oggetto
riferito da un puntatore (Sintassi 7.3).
Sintassi 7.3 Accesso al contenuto di un oggetto riferito da un puntatore.
*ptr_identifier;
Ritornando al precedente esempio, lo Snippet 7.3 assegna alla
variabile tmp
il contenuto della variabile
value
riferita dal puntatore data
.
Snippet 7.3 Utilizzo dell’operatore di deriferimento con un puntatore.
int value = 10;
int *data = &value; // data conterrà l'indirizzo di value
int tmp = *data; // tmp = 10
In buona sostanza l’istruzione *data
può essere
espressa letteralmente nel seguente modo: “accedi al contenuto
dell’oggetto puntato da data
e non al contenuto di
data stesso”.
Da questo punto di vista, quindi, *data
può essere
considerato un alias di value
, ossia
qualsiasi manipolazione effettuata per il tramite di esso si
ripercuoterà su value
stessa (Snippet 7.4 e Figura
7.5).
Snippet 7.4 Manipolazione tramite un puntatore dell’oggetto puntato.
int value = 10;
int *data = &value;
*data = 100; // value = 100
ATTENZIONE
Non applicare mai l’operatore di deriferimento a un puntatore non inizializzato con un corretto indirizzo di memoria, altrimenti si potrà avere un comportamento non definito: crash del programma (l’indirizzo di memoria memorizzato nel puntatore è al di fuori dell’address space valido del programma), stampa di valori garbage o insensati (l’indirizzo di memoria memorizzato nel puntatore mostra quello che in quel momento è presente a quell’indirizzo quantunque valido) e così via.
Riepilogando, per utilizzare correttamente un puntatore, bisogna compiere le seguenti fondamentali operazioni.
- Utilizzo dell’operatore di indirezione
*
durante la fase di dichiarazione di un puntatore; per esempioint *ptr_data
. - Assegnamento di un indirizzo di memoria valido, tramite
l’operatore di indirizzamento
&
, di un oggetto dello stesso tipo di quello indicato durante la fase di dichiarazione di un puntatore; per esempioptr_data = &value
(con value di tipoint
). - Utilizzo dell’operatore di indirezione
*
durante la fase di manipolazione dell’oggetto riferito da un puntatore; per esempio*ptr_data = 100
.
DETTAGLIO
Perché al punto 2 si è precisato che l’indirizzo
di memoria assegnato a un puntatore deve essere di un oggetto dello
stesso tipo da esso espresso in fase di dichiarazione? Perché un
puntatore, indipendentemente dall’oggetto puntato, conterrà sempre
e solo un indirizzo di memoria; pertanto, indicando durante la sua
dichiarazione qual è il tipo di oggetto localizzato a
quell’indirizzo di memoria, si otterrà che, in fase di accesso, il
contenuto di tale locazione sarà interpretato correttamente. Per
esempio, compilando e mandando in esecuzione il Listato 7.1 avremo
sia il messaggio: warning: assignment from incompatible
pointer type
, sia un output dove il valore della variabile
di tipo float
non sarà stato interpretato
correttamente.
Listato 7.1 IncompatibleTypes.c (IncompatibleTypes).
/* IncompatibleTypes.c :: Tipi incompatibili con i puntatori :: */
#include <stdio.h>
#include <stdlib.h>
int main(void)
{
int i_number = 100;
float f_number = 100.44f;
int *ptr_i_number = &i_number; // OK tipi compatibili
printf("Valore di i_number: %d\n", *ptr_i_number);
ptr_i_number = &f_number; // ATTENZIONE tipi non compatibili
printf("Valore di f_number: %f\n", *ptr_i_number);
return (EXIT_SUCCESS);
}
Output 7.1 Dal Listato 7.1 IncompatibleTypes.c.
Valore di i_number: 100
Valore di f_number: 0.000000
Puntatori come parametri di funzioni
Quando si definisce una funzione è possibile dichiarare dei parametri che sono di tipo puntatore, cioè delle variabili che sono in grado di contenere degli indirizzi di memoria dei corrispettivi argomenti (Sintassi 7.4 e 7.5).
Sintassi 7.4 Prototipo di funzione con un parametro di tipo puntatore.
return_type function_identifier(data_type *ptr_identifier); // con identificatore
return_type function_identifier(data_type *); // senza identificatore
Sintassi 7.5 Definizione di una funzione con un parametro di tipo puntatore.
return_type function_identifier(data_type *ptr_identifier) { ... }
Questa modalità di progettazione delle funzioni consente di “aggirare” il problema del passaggio per valore degli argomenti poiché i parametri sono in grado, indirettamente, di modificarne i valori.
Per il passaggio dei puntatori come argomenti di una funzione è
possibile usare la seguente modalità (Sintassi 7.6) dove, cioè, si
passa direttamente un oggetto che è già di per sé un tipo puntatore
oppure si antepone l’operatore di indirizzamento &
a un determinato oggetto.
Sintassi 7.6 Invocazione di una funzione passando come argomento un puntatore.
function_identifier(ptr_identifier); // ptr_identifier è un puntatore
function_identifier(&identifier);// identifier è un qualsiasi oggetto
NOTA
In C gli argomenti sono passati, sempre, per valore anche se sono dei puntatori. Il fatto che un parametro sia un puntatore e che consenta di modificare il relativo argomento non implica che esista, nativamente, la modalità di passaggio di un argomento “per riferimento” (come in C++) o “per indirizzo”. Infatti, quando si passa a una funzione l’indirizzo del suo argomento è più corretto dire che si sta passando “un suo riferimento” e non che l’argomento è passato “per riferimento”.
Listato 7.2 PointersAndPassByValue.c (PointersAndPassByValue).
/* PointersAndPassByValue.c :: Pass by value e puntatori :: */
#include <stdio.h>
#include <stdlib.h>
void foo(int *p);
int main(void)
{
int a = 10;
int *j = &a;
// per stampare 0x e l'indirizzo si sarebbe potuto usare anche %#p
printf("Indirizzo riferito da j [ 0x%p ] PRIMA del passaggio dell'argomento.\n", j);
foo(j); // passo un puntatore...
// per stampare 0x e l'indirizzo si sarebbe potuto usare anche %#p
printf("Indirizzo riferito da j [ 0x%p ] DOPO il passaggio dell'argomento "
"e p = &k;.\n", j);
return (EXIT_SUCCESS);
}
void foo(int *p)
{
int k = 100;
p = &k; // ok j non è interessato... pass by value
}
Output 7.2 Dal Listato 7.2 PointersAndPassByValue.c.
Indirizzo riferito da j [ 0x0028fee8 ] PRIMA del passaggio dell'argomento.
Indirizzo riferito da j [ 0x0028fee8 ] DOPO il passaggio dell'argomento e p = &k;.
Il Listato 7.2 mostra come anche un puntatore sia passato by
value: infatti, quando nel main
viene invocata la
funzione foo
viene fatta una copia dell’indirizzo di
memoria contenuto nel puntatore j
, e tale copia viene
posta nel parametro p
, anch’esso un puntatore.
A questo punto entrambi i puntatori referenziano lo stesso
oggetto, ovvero la variabile a
.
Poi, nell’ambito della funzione foo
, al puntatore
p
viene assegnato un altro indirizzo di memoria,
quello della variabile k
, senza però che tale
assegnamento incida sull’indirizzo di memoria dell’argomento
j
che rimane, infatti, inalterato.
A questo punto il puntatore j
continua a puntare
alla variabile a
mentre il puntatore p
“rompe” il puntamento verso la variabile a
e imposta
un nuovo puntamento verso la variabile k
(Figura
7.6).
Scorrendo il sorgente è interessante notare l’utilizzo,
nell’ambito della funzione printf
, dello specificatore
di formato %p
che consente di stampare, in modo
dipendente dall’implementazione, un numero esadecimale che
rappresenta il valore della locazione di memoria contenuta nel
puntatore.
Mostriamo, infine, la scrittura di una funzione
swap
(Listato 7.3) che correttamente, grazie
all’impiego dei puntatori, consente di scambiare i valori di due
variabili passate come argomenti per il tramite dei corrispettivi
parametri.
Listato 7.3 SwapWithPointers.c (SwapWithPointers).
/* SwapWithPointers.c :: Scambio di valori con l'uso dei puntatori :: */
#include <stdio.h>
#include <stdlib.h>
/* prototipo di swap */
void swap(int *w, int *z);
int main(void)
{
int a = 10, b = 20;
printf("a e b prima dello swap: a=%d - b=%d\n", a, b);
// passo i puntatori ad a e b
swap(&a, &b);
printf("a e b dopo lo swap: a=%d - b=%d\n", a, b);
return (EXIT_SUCCESS);
}
/* definizione di swap */
void swap(int *w, int *z)
{
int tmp = *w;
*w = *z;
*z = tmp;
}
Output 7.3 Dal Listato 7.3 PointersAndPassByValue.c.
a e b prima dello swap: a=10 - b=20
a e b dopo lo swap: a=20 - b=10
TERMINOLOGIA
Quando si passa a una funzione un argomento tipo
&a
si può anche direttamente dire che si sta
passando un puntatore ad a
piuttosto che l’indirizzo
di a
perché, dato che l’operatore &
genera l’indirizzo di una variabile, &a
ne
rappresenta un puntatore.
Puntatori come valori di ritorno dalle funzioni
Una funzione può essere anche definita con la possibilità di
avere come valore di ritorno un tipo puntatore; per compiere
quest’operazione è sufficiente dichiarare il puntatore nel consueto
modo, ossia tipo di dato, simbolo *
e identificatore,
e porlo come tipo di ritorno prima dell’identificatore della
relativa funzione (Snippet 7.5).
Snippet 7.5 Funzione che ritorna un tipo puntatore.
int *foo(void) { … }
La funzione foo
dello Snippet 7.5 è definita con un
valore di ritorno che è un puntatore a un int
, cioè
dovrà ritornare un indirizzo di memoria di un oggetto dello stesso
tipo di dato.
Quando si definisce una funzione in questo modo bisogna prestare attenzione a non ritornare mai un indirizzo di memoria di una variabile locale automatica alla funzione stessa: questo perché, quando la funzione ritorna, tale variabile cesserà di esistere e pertanto il relativo puntatore sarà considerato invalido (Listato 7.4).
Listato 7.4 ReturningAPointer.c (ReturningAPointer).
/* ReturningAPointer.c :: Ritorno di un puntatore a una variabile locale :: */
#include <stdio.h>
#include <stdlib.h>
int *foo(void);
void bar(void);
int main(void)
{
int *p = foo();
printf("Valore di j per il tramite di *p: %d\n", *p);
bar(); // invoco un'altra funzione
printf("Valore di j per il tramite di *p: %d\n", *p);
return (EXIT_SUCCESS);
}
int *foo(void)
{
int j = 1000;
printf("Indirizzo di j in foo: %#p\n", &j);
return &j;
}
void bar(void)
{
int b = 2000;
printf("Indirizzo di b in bar: %#p\n", &b);
}
Output 7.4 Dal Listato 7.4 ReturningAPointer.c.
Indirizzo di j in foo: 0x28febc
Valore di j per il tramite di *p: 1000
Indirizzo di b in bar: 0x28febc
Valore di j per il tramite di *p: 2000
Il Listato 7.4 definisce la funzione foo
che
ritorna un puntatore alla variabile locale j
lì
definita, la quale contiene il valore 1000
, e la
funzione bar
, che non ritorna nulla e che definisce la
variabile locale b
, che contiene il valore
2000
.
La relativa funzione main
invoca subito la funzione
foo
che ritorna nel puntatore p
l’indirizzo della variabile locale j
. Poi, per il
tramite del puntatore p
, ne stampa il valore che è
congruo, ossia vale ancora 1000
.
Successivamente invoca la funzione bar
e poi, alla
sua uscita, stampa di nuovo, sempre per mezzo del puntatore
p
, il valore riferito, che però questa volta non è più
congruo, ossia non vale più 1000
ma
2000
.
Il motivo di questo comportamento è spiegabile analizzando
l’output del programma, dove si nota che quando viene invocata la
funzione foo
il compilatore crea uno stack frame dove
pone la variabile locale j
all’indirizzo di memoria
0x28febc
che è ritornato al puntatore p
.
Quando però la funzione foo
termina, lo stack frame
relativo viene rimosso dal function call stack e la variabile
locale j
non esiste più e l’indirizzo di memoria
0x28febc
diviene invalido (potrà a questo punto essere
usato per allocare altri dati), pur continuando a contenere il
valore 1000
come mostrato dalla successiva istruzione
printf
invocata nel main
.
A questo punto viene invocata la funzione bar
e
viene creato un altro stack frame dove la variabile locale
b
viene allocata all’indirizzo 0x28febc
ancora disponibile e utilizzabile (è lo stesso della ex
variabile j
) e lì viene posto il valore
2000
.
All’uscita della funzione bar
, però, il suo stack
frame viene rimosso dal function call stack e la variabile
b
viene distrutta; quando nel main
è
invocata in seguito la funzione printf
, il puntatore
p
, puntando ancora all’indirizzo di memoria
0x28febc
, stampa l’ultimo valore trovato, ossia
2000
.
In pratica l’indirizzo di memoria 0x28febc
impiegato per le variabili locali citate può essere utilizzato da
ogni invocazione di un nuova funzione per le proprie necessità;
questo spiega il problema della “sovrascrittura” del valore che in
origine era di j
.
NOTA
Il compilatore GCC, quando si compila il sorgente
del Listato 7.4, emetterà il seguente avviso: warning:
function returns address of local variable
. Un buon
compilatore dovrebbe dare sempre un avviso di questo tipo per
evitare che un programmatore possa utilizzare un puntatore a un
indirizzo di memoria non valido.
Puntatori e array
Uno degli aspetti tra i più interessanti di C è la stretta
correlazione tra puntatori e array. Dato un array come, per
esempio, data
, la sua valutazione ritorna un indirizzo
di memoria, ossia un puntatore al suo primo elemento che si può
quindi assegnare come valore a un puntatore dello stesso tipo.
Snippet 7.6 Valutazione del nome di un array.
int data[] = {10, 100, 20, 40, 50, 60, 70};
int *ptr_to_data = data;
Lo Snippet 7.6 definisce l’array data
deputato a
contenere 7 elementi di tipo intero e poi ne assegna l’indirizzo
del primo elemento (l’elemento 0 con valore 10
) al
puntatore a int
denominato
ptr_to_data
.
L’assegnamento di data
a ptr_to_data
è
equivalente al seguente che, comunque, anche se più esplicito e
chiaro è raramente usato: int *ptr_to_data =
&data[0]
.
Quanto sopra fa conseguire che qualsiasi accesso a un elemento
di un array ottenibile mediante la nota sintassi che fa uso
dell’operatore di subscript [ ]
e di un indice è anche
ottenibile tramite un puntatore e la possibilità di “aggiungere” o
“sottrarre” da esso un valore numerico che indica un valore di
“scostamento” (o offset) rispetto all’attuale indirizzo lì
contenuto (Snippet 7.7).
Snippet 7.7 Utilizzo di un puntatore per accedere a un elemento di un array riferito.
int data[] = {10, 100, 20, 40, 50, 60, 70};
int *ptr_to_data = data;
// fa puntare ptr_to_data al quarto elemento dell'array
ptr_to_data += 3;
Lo Snippet 7.7 evidenzia come aggiungendo il valore
3
all’attuale indirizzo in memoria di
ptr_to_data
lo faccia puntare all’indirizzo di memoria
del quarto elemento dell’array data
referenziato (in
pratica all’indirizzo di “base” di 0x0041f7bc
sono
state aggiunte 3 “unità di storage di scostamento” che l’hanno
fatto incrementare al valore di 0x0041f7c8
).
È qui importante precisare che gli scostamenti sono espressi in
unità di storage perché ogni tipo di dato necessità di una
determinata quantità di memoria per allocare il corrispondente
valore (per esempio, su un sistema a 32 bit un int
richiede 4 byte per memorizzare un intero); pertanto aggiungere 1
unità a un puntatore a int
significa spostare il suo
indirizzo di 4 byte e non di 1 byte.
Ritornando al nostro esempio, l’espressione ptr_to_data +=
3
sposterà il puntatore di 12 byte (ossia 3 * 4
byte con un int
di 32 bit) facendolo, quindi, puntare
all’elemento 3 dell’array data
.
Facciamo ora un ulteriore passo in avanti che mostra
l’equivalenza tra l’aritmetica degli indici propria di un array e
quella propria dei puntatori. Un’espressione come
data[i]
si può scrivere anche come *(data +
i);
ossia: data
, essendo di fatto un puntatore
al primo elemento di un array, farà sì che data + i
punterà all’i-esimo elemento di quell’array e l’operatore
di deriferimento *
consentirà di ottenere il relativo
valore.
In linea generale è dunque possibile asserire che
&data[i]
e data + i
sono sinonimi,
poiché entrambi sono puntatori all’i-esimo elemento di
data
.
Per contro, è possibile utilizzare la notazione propria degli
array anche con un puntatore: per esempio, per accedere
all’elemento 5 dell’array data
per il tramite di
ptr_to_data
, possiamo scrivere anche
ptr_to_data[5]
.
ATTENZIONE
L’equivalenza tra un array e un puntatore è solo
relativa alla possibilità di usare in modo intercambiabile
l’aritmetica dei puntatori con l’indicizzazione degli array. Tra di
essi vi è comunque un’importante differenza: il nome di un array è
un lvalue non modificabile (per esempio è un errore scrivere
data++
), mentre un puntatore è un lvalue modificabile
(per esempio è lecito scrivere ptr_to_data++
). Quindi,
un nome di un array non è un puntatore; sono, infatti,
tecnicamente, due oggetti distinti ed è solo in un determinato
contesto valutativo che il compilatore converte un array
in un puntatore.
CURIOSITÀ
È lecito scrivere qualcosa come
i[data]
al posto di data[i]
? La risposta
è certamente affermativa perché il compilatore, per effetto delle
equivalenze discusse tra array e puntatori, quando incontra
un’espressione come questa la trasforma in *(i +
data)
, che è dunque corretta. Allo stesso modo
data[i]
sarebbe trasformata in *(data +
i)
.
Aritmetica dei puntatori
L’aritmetica dei puntatori (pointer arithmetic), conosciuta anche come aritmetica degli indirizzi (address arithmetic), dato un puntatore o dei puntatori che puntano a elementi di un array, consente di compiere le seguenti operazioni.
- Aggiungere un valore intero a un puntatore: consente
di incrementare l’attuale indirizzo di memoria riferito dal
puntatore di tante unità di storage quante indicate dal valore
fornito. Così, se
ptr_to_data
punta all’elementox
dell’arraydata
(data[x]
), aggiungerey
a esso (ptr_to_data + y
) lo farà puntare all’elemento didata
postoy
unità di storage dopo l’attuale indirizzo diptr_to_data
(data[x + y]
).
Snippet 7.8 Aggiungere un valore intero a un puntatore (Figura 7.9).
int x = 2;
int y = 3;
int data[] = {10, 100, 20, 40, 50, 60, 70};
int *ptr_to_data = &data[x]; // punta all'elemento 2 con valore 20
ptr_to_data = ptr_to_data + y; // ora punta all'elemento 5 con valore 60
- Sottrarre un valore intero da un puntatore: consente
di decrementare l’attuale indirizzo di memoria riferito dal
puntatore di tante unità di storage quante indicate dal valore
fornito. Così, se
ptr_to_data
punta all’elementox
dell’array data (data[x]
), sottrarrey
da esso (ptr_to_data - y
) lo farà puntare all’elemento di data postoy
unità di storage prima dell’attuale indirizzo diptr_to_data
(data[x - y
]).
Snippet 7.9 Sottrarre un valore intero da un puntatore (Figura 7.10).
int x = 5;
int y = 4;
int data[] = {10, 100, 20, 40, 50, 60, 70};
int *ptr_to_data = &data[x]; // punta all'elemento 5 con valore 60
ptr_to_data = ptr_to_data - y; // ora punta all'elemento 2 con valore 100
- Sottrarre un puntatore da un altro puntatore: consente
di ottenere la distanza, in elementi dell’array, tra due puntatori
che puntano a indirizzi di memoria dell’array medesimo. Così, se
ptr_to_data
punta all’elementox
dell’arraydata
(data[x]
) eptr_to_data_2
punta all’elementoy
dell’array data (data[y]
), sottrarreptr_to_data_2
da esso (ptr_to_data – ptr_to_data_2
) oppure sottrarreptr_to_data
daptr_to_data_2
(ptr_to_data_2 – ptr_to_data
) restituirà il numero di elementi di distanza, rispettivamente, in “negativo” nel primo caso e in “positivo” nel secondo caso.
Snippet 7.10 Sottrarre un puntatore da un altro puntatore (Figura 7.11).
int x = 1;
int y = 3;
int data[] = {10, 100, 20, 40, 50, 60, 70};
int *ptr_to_data = &data[x]; // punta all'elemento 1 con valore 100
int *ptr_to_data_2 = &data[y]; // punta all'elemento 3 con valore 40
ptrdiff_t distance_1 = ptr_to_data - ptr_to_data_2; // -2
ptrdiff_t distance_2 = ptr_to_data_2 - ptr_to_data; // 2
Lo Snippet 7.10 mostra come il tipo utilizzato per contenere la
differenza tra due puntatori sia ptrdiff_t
, il quale è
definito nel file header <stddef.h>
(generalmente con un typedef
di un tipo intero con
segno) ed è il modo “portabile” per esprimere tale differenza (con
la funzione printf
il correlativo specificatore di
formato utilizzabile è, per esempio, %td
).
ATTENZIONE
Se si compiono le operazioni di aritmetica dei puntatori qui citate con un puntatore che non punta a un elemento di un array oppure con dei puntatori che non riferiscono elementi di uno stesso array (come è il caso di sottrazione di un puntatore da un altro), il comportamento sarà non definito. In ogni caso l’indirizzo di memoria subito successivo a quello dell’ultimo elemento di un array è garantito essere valido.
Comparazione tra puntatori
Oltre alle operazioni proprie dell’aritmetica dei puntatori è
possibile anche comparare dei puntatori mediante l’utilizzo degli
operatori relazionali (<
, <=
,
>
, >=
) e di uguaglianza
(==
, !=
); ciò consente di sapere se, dati
due puntatori che puntano a elementi dello stesso array, uno è
minore o maggiore di un altro oppure se uno è uguale o diverso da
un altro (in quest’ultimo caso gli operatori ==
e
!=
possono essere usati anche con puntatori dello
stesso tipo anche se non riferiscono elementi dello stesso
array).
I criteri di confronto sono effettuati sugli indirizzi di
memoria lì contenuti. Così, se ptr_to_data
punta
all’elemento x
dell’array data
(data[x]
) e ptr_to_data_2
punta
all’elemento y
dell’array data
(data[y]
), utilizzare l’operatore minore di
(ptr_to_data < ptr_to_data_2
) ritornerà il valore
1
se l’indirizzo di memoria riferito da
ptr_to_data
sarà inferiore all’indirizzo di memoria
riferito da ptr_to_data_2
(e il valore 0
in caso contrario). Anche per gli altri operatori il risultato del
confronto sarà sempre 1
oppure 0
.
Snippet 7.11 Comparazione di due puntatori (Figura 7.12).
int x = 4;
int y = 6;
int data[] = {10, 100, 20, 40, 50, 60, 70};
int *ptr_to_data = &data[x]; // punta all'elemento 4 con valore 50
int *ptr_to_data_2 = &data[y]; // punta all'elemento 6 con valore 70
// ptr_to_data -> indirizzo: 0x0041f7cc
// ptr_to_data_2 -> indirizzo: 0x0041f7d4
_Bool res = ptr_to_data < ptr_to_data_2; // 1 cioè vero!
ATTENZIONE
Se si compiono le operazioni di comparazione dei puntatori qui citate con gli operatori relazionali, con dei puntatori che non riferiscono elementi di uno stesso array, il comportamento sarà non definito. Tuttavia l’indirizzo di memoria subito successivo a quello dell’ultimo elemento di un array è garantito essere valido e può essere utilizzato per un confronto.
Puntatori e parametri di una funzione di tipo array
Quando definiamo una funzione con un parametro formale di tipo
array, per il compilatore esso è automaticamente interpretato come
un puntatore al tipo di dato dell’array; ossia, se un parametro
formale è scritto come int data[]
, per il compilatore
lo stesso sarà trattato come int *data
, ossia un
puntatore a un int
.
Quanto evidenziato è comunque perfettamente lecito perché quando
si passa, poi, come parametro attuale il nome di un array (per
esempio numbers
), quello che si fornisce è l’indirizzo
di memoria del suo primo elemento, ossia un puntatore all’elemento
0.
IMPORTANTE
È solo nell’ambito della definizione di un
parametro formale di una funzione che per C vi è un’equivalenza
tra, per esempio, int data[]
e int *data
.
Il parametro data
è cioè sempre considerato come un
puntatore a un int
(Listato 7.5).
Listato 7.5 ArrayAsPointerInParameterDefinition.c (ArrayAsPointerInParameterDefinition).
/* ArrayAsPointerInParameterDefinition.c :: Array vs puntatori come parametri di una funzione :: */
#include <stdio.h>
#include <stdlib.h>
#define SIZE 6
// prototipo della funzione subtraction
// equivalenti --> int subtraction(int *data, int length);
// int subtraction(int [], int length);
// int subtraction(int *, int length);
int subtraction(int data[], int length);
int main(void)
{
int some_data[] = {369, 10, 15, 65, 88, 66};
printf("L'array some_data, nella funzione main, ha una dimensione di "
"%zu byte\n", sizeof some_data);
int res = subtraction(some_data, SIZE);
printf("Il risultato della sottrazione di tutti gli elementi di "
"some_data e': %d\n", res);
return (EXIT_SUCCESS);
}
// definizione della funzione subtraction
// equivalente --> int subtraction(int *data, int length) { ... }
int subtraction(int data[], int length)
{
printf("\"L'array data\", nella funzione subtraction, ha una dimensione "
"di %zu byte\n", sizeof data);
int result = *data;
// utilizzo dell'aritmetica dei puntatori per scorrere un array...
for (int *p = data + 1; p < data + length; p++)
result -= *p;
return result;
}
Output 7.5 Dal Listato 7.5 ArrayAsPointerInParameterDefinition.c.
L'array some_data, nella funzione main, ha una dimensione di 24 byte
"L'array data", nella funzione subtraction, ha una dimensione di 4 byte
Il risultato della sottrazione di tutti gli elementi di some_data e': 125
Il Listato 7.5 definisce una funzione subtraction
che, dato un array come argomento, ne restituisce un valore che è
la differenza di tutti i valori dei suoi elementi.
L’importanza del programma del listato non risiede di sicuro nella funzione di sottrazione, che è banale, quanto piuttosto perché evidenzia due aspetti di rilievo: il primo è legato alla verifica che nell’ambito della definizione di una funzione un parametro di tipo array è di fatto considerato come un puntatore a un suo elemento; il secondo è relativo a come è possibile utilizzare l’aritmetica dei puntatori per scorrere gli elementi di un array riferito da un puntatore in sostituzione della consueta indicizzazione.
Per quanto riguarda il primo aspetto, lo stesso è verificabile guardando all’output del programma, dove:
- l’operatore
sizeof
applicato all’arraysome_data
nell’ambito della funzionemain
dà come risultato il valore24
in accordo con il fatto che è un tipo array di int che contiene 6 elementi di tipoint
di 4 byte ciascuno sul corrente sistema a 32 bit; - l’operatore
sizeof
applicato nell’ambito della funzionesubtraction
dà come risultato il valore4
in accordo con il fatto che è un tipo puntatore a un int, e dunque sul corrente sistema a 32 bit 4 byte sono lo spazio utilizzato per allocare un tipo puntatore atto a contenere un indirizzo di memoria.
Il secondo aspetto è invece dimostrabile nel ciclo
for
della funzione subtraction
: qui si
utilizza espressamente l’aritmetica dei puntatori per manipolare il
parametro data
fornendo al puntatore p
l’indirizzo dell’elemento 1 (data + 1
) e verificando,
come condizione di continuazione del ciclo, che l’indirizzo
corrente di p
sia nel range di indirizzi dove sia
possibile validamente ottenere un valore numerico da computare
(p < data + length
) mediante l’operatore di
deriferimento (result -= *p
).
Puntatori e array multidimensionali
Dato un array multidimensionale, come per esempio quello a due dimensioni definito nello Snippet 7.12 e con la rappresentazione in memoria fornita dalla Figura 7.13, possiamo tracciare, al pari di quanto già fatto per gli array monodimensionali, come si relazioni rispetto a un puntatore.
Snippet 7.12 Definizione di un array bidimensionale.
int data[2][3] = // 2 righe per 3 colonne
{
{1,2,3}, // I riga
{-1, -2, -3} // II riga
};
Prima di procedere oltre ricordiamo che per C: un array a due dimensioni è un array di array, ossia un array a una dimensione dove ogni elemento è esso stesso un altro array; la disposizione in memoria di un array a due dimensioni è fatta riga per riga (row major order, prima la riga 0, poi la riga 1 e così via per tutte le altre righe).
NOTA
Per semplicità useremo gli array a due dimensioni come forma di array multidimensionale. I concetti generali esposti saranno comunque validi anche per array di dimensioni maggiori.
Nel prossimo elenco riportiamo la “valutazione” da parte del
compilatore dell’identificatore data
, ossia a che tipo
di puntatore “corrisponde” quando gli applichiamo o meno
l’operatore di indirizzamento &.
data
corrisponde aint (*)[3]
, ossia a un puntatore a un array di 3 elementi. Esso ritorna l’indirizzo di memoria dell’elemento 0 (0x00a2fbac
) che è in pratica visualizzabile come la riga 0 della matrice contenente i valori1
,2
e3
.&data[0]
corrisponde aint (*)[3]
, ossia a un puntatore a un array di 3 elementi. Esso ritorna l’indirizzo di memoria dell’elemento 0 (0x00a2fbac
) che è in pratica visualizzabile come la riga 0 della matrice contenente i valori1
,2
e3
. Quindi,data
e&data[0]
sono “sinonimi”.data[0]
corrisponde aint *
, ossia a un puntatore a unint
. Esso ritorna l’indirizzo di memoria dell’elemento 0 (0x00a2fbac
) che è in pratica visualizzabile come la riga 0 della matrice contenente i valori1
,2
e3
.&data[0][0]
corrisponde aint *
, ossia a un puntatore aint
. Esso ritorna l’indirizzo di memoria (0x00a2fbac
) dell’elemento che si trova alla riga 0 e alla colonna 0;&data
corrisponde aint (*)[2][3]
, ossia a un puntatore a un array di 2 righe per 3 colonne. Esso ritorna l’indirizzo di memoria (0x00a2fbac
) che è in pratica quello a partire dal quale inizia tutto l’array a due dimensioni.
Le espressioni viste sono importanti perché evidenziano che, pur
ritornando tutte lo stesso indirizzo di memoria, avranno comunque
dei tipi di puntatori differenti, e ciò ci sarà utile per
comprendere cosa accadrà quando dovremo applicare l’operatore di
deriferimento *
su di esse. Avremo infatti quanto
segue
*data
corrisponde aint *
, ossia a un puntatore a unint
. Esso ritorna l’indirizzo di memoria dell’elemento 0 (0x00a2fbac
) che è in pratica visualizzabile come la riga 0 della matrice contenente i valori1
,2
e3
.*&data[0]
corrisponde aint *
, ossia a un puntatore a unint
. Esso ritorna l’indirizzo di memoria dell’elemento 0 (0x00a2fbac
) che è in pratica visualizzabile come la riga 0 della matrice contenente i valori1
,2
e3
.*data[0]
corrisponde a unint
. Esso ritorna il valore dell’elemento, ossia 1, posto alla colonna 0 e riga 0.*&data[0][0]
corrisponde a unint
. Esso ritorna il valore dell’elemento, ossia1
, posto alla colonna 0 e riga 0.*&data
corrisponde aint (*)[3]
, ossia a un puntatore a un array di 3 elementi. Esso ritorna l’indirizzo di memoria dell’elemento 0 (0x00a2fbac
) che è in pratica visualizzabile come la riga 0 della matrice contenente i valori1
,2
e3
.
In più se si vuole utilizzare in modo assoluto la notazione puntatore/offset in alternativa a quella propria degli array allora avremo che:
data + r
sposta il puntatore alla riga indicata dar
e ritorna un tipoint (*)[3]
. Per esempio,data + 1
sposterà il puntatore alla riga 1 ossia all’indirizzo0x00a2fbb8
. Ciò avviene perchédata
è un puntatore a un array di 3 colonne e pertanto aggiungere un’unità di storage a esso ne farà incrementare l’indirizzo di 12 byte (3 elementi della riga per la dimensione di 4 byte propria di unint
sul corrente sistema di 32 bit);*(data + r)
sposta il puntatore alla riga indicata dar
e poi la dereferenzia ritornando un tipoint *
. Per esempio,*(data + 1)
sposterà il puntatore alla riga 1 e tornerà un riferimento al suo primo elemento posto all’indirizzo0x00a2fbb8
;*(data + r) + c
sposta il puntatore alla riga indicata dar
, poi la dereferenzia ritornando l’indirizzo di memoria dell’elemento 0 come tipoint *
che poi sposta dic
unità di storage. Per esempio,*(data + 1) + 1
sposterà il puntatore alla riga 1 e colonna 1, ossia all’indirizzo0x00a2fbbc
. Ciò avviene perché il puntatore da spostare si riferisce a quello che “muove” le colonne dove ogni unità di storage vale 4 byte (ossia unint
di 32 bit sull’attuale sistema);*(*(data + r) + c)
sposta il puntatore alla riga indicata dar
, poi la dereferenzia ritornando l’indirizzo di memoria dell’elemento 0 come tipoint *
che poi sposta dic
unità di storage. Infine, dereferenzia tale indirizzo di memoria ritornando un tipoint
. Per esempio*(*(data + 1) + 1)
, ritornerà il valore-2
che è posto nella colonna 1 della riga 1.
Quanto mostrato sinora è senza dubbio complesso e richiede una certa attenzione e pazienza per comprendere in modo ottimale la relazione tra un puntatore e un array bidimensionale; comunque ciò non deve indurre a eccessive preoccupazioni perché, quando si scrivono programmi che fanno uso di array bidimensionali, è sufficiente:
- per accedere a un determinato elemento, utilizzare la più
semplice notazione con indicizzazione propria degli array (per
esempio,
data[1][1]
accede alla colonna 1 della riga 1); - per impiegare un parametro formale di tipo array
bidimensionale, dichiararlo con la consueta notazione con
indicizzazione degli array già vista (per esempio
void foo(int data[][3]) { … }
) oppure con la notazione che fa uso dei puntatori (per esempiovoid foo(int (*data)[3]) { … }
).
ATTENZIONE
Le parentesi ( )
intorno a
*data
sono essenziali. Se le omettessimo, la
dichiarazione relativa, ossia int *data[3]
, per
effetto della più alta precedenza dell’operatore [ ]
rispetto all’operatore *
, significherebbe che
data
è un array di 3 puntatori a int
.
Invece, scrivere int (*data)[3]
fa sì che
l’espressione significhi che data
è un puntatore a un
array di 3 elementi di tipo int
.
Listato 7.6 PointerToArrayAsParameter.c (PointerToArrayAsParameter).
/* PointerToArrayAsParameter.c :: Puntatori come parametri di una funzione per array 2d :: */
#include <stdio.h>
#include <stdlib.h>
#define ROWS 3
#define COLS 5
/* prototipo della funzione search */
// equivalente -> int search(int (*ptr_to_data)[COLS], int rows);
int search(int (*)[COLS], int);
int main(void)
{
int data[][COLS] =
{
{1, 2, 3, 4, 5},
{-4, -6, 10, 2, 9},
{100, -100, 33, 34, 24}
};
// invocazione di search
int res = search(data, ROWS);
printf("La matrice data contiene %d numeri negativi!\n", res);
return (EXIT_SUCCESS);
}
/* definizione della funzione search */
int search(int (*ptr_to_data)[COLS], int rows)
{
int nr = 0;
// qui r++ fa spostare alla riga successiva della matrice perché
// ptr_to_data è di tipo int (*)[5]
for (int r = 0; r < rows; r++)
{
// qui c++ fa spostare alla colonna successiva della riga corrente perché
// *(ptr_to_data + r) è di tipo int *
for (int c = 0; c < COLS; c++)
{
// sintassi alternativa puntatore/offset a quella propria degli array
int val = *(*(ptr_to_data + r) + c);
if (val < 0)
nr++;
}
}
return nr;
}
Output 7.6 Dal Listato 7.6 PointerToArrayAsParameter.c.
La matrice data contiene 3 numeri negativi!
Il Listato 7.6 esplicita in modo pratico, nell’ambito della
funzione search
, come utilizzare la notazione
puntatore/offset per riferirsi agli elementi di una matrice. Il
ciclo for
più esterno si occupa di far spostare il
puntatore corrente alla riga successiva; lo fa incrementando
ptr_to_data
di una unità di storage alla volta che è
pari a 20 byte perché ogni riga contiene 5 elementi di tipo
int
con un int
di 4 byte (ricordiamo che
ptr_to_data
è di tipo int (*)[5]
). Il
ciclo for più interno, invece, fa spostare il corrente puntatore
alla colonna successiva, e lo fa incrementando *(ptr_to_data
+ r)
di una unità di storage alla volta che è pari a 4 byte
perché ogni colonna è un elemento di tipo int
con un
int
di 4 byte (ricordiamo che *(ptr_to_data +
r)
è di tipo int *
).
Infine, applica di nuovo l’operatore di deriferimento sul puntatore ritornato in modo da ottenere il valore della corrente colonna.
Per comprendere in modo meno astratto quanto detto, la Figura
7.14 mostra dove si troverà il puntatore ptr_to_data
quando r
varrà 1
e c
varrà
2
, ossia sulla colonna 2 della riga 1 (elemento con
valore 10
).
Per quanto concerne, infine, la scrittura dell’array
bidimensionale come parametro di una funzione, possiamo ora
comprendere perché una sintassi come int data[][]
non
sarebbe mai accettata dal compilatore; dato che esso “converte” la
notazione a indicizzazione degli array come notazione a puntatore,
quando valuterà qualcosa come data + 1
non avrà dati a
sufficienza per sapere di quante unità di storage spostare il
puntatore corrente alla prossima riga.
Ecco, quindi, perché bisogna sempre indicare il numero di
colonne dell’array mentre non è obbligatorio indicare il numero di
righe (il parametro int data[][COLS]
è trattato dal
compilatore come int (*data)[COLS]
, e infatti il
relativo argomento passato è un puntatore a un array di
COLS
colonne).
Array di puntatori
Gli array di puntatori sono vettori dove ciascun elemento è un
puntatore a un determinato tipo. Così, una dichiarazione come
int *data[4]
stabilisce che il nome data
è un array di 4 elementi ciascuno dei quali contiene come valore un
indirizzo di memoria di un tipo int
ossia un suo
puntatore.
Questo tipo di oggetto si presta in modo ottimale a creare i cosiddetti array triangolari o irregolari, ossia array dove ogni riga può avere un numero di colonne differente.
Snippet 7.13 Un array irregolare.
// array di 4 puntatori a int
int *data[] =
{
(int[]) {1, 2}, // 2 colonne
(int[]) {3, 4, 5}, // 3 colonne
(int[]) {6, 7, 8, 9}, // 4 colonne
(int[]) {10, 11, 12, 13, 14} // 5 colonne
};
Lo Snippet 7.13 crea l’array data
composto da 4
righe dove ciascuna riga punta a un array di colonne, definito con
la sintassi “letterale”, di differente dimensione.
Poiché il nome di un array è valutato come un puntatore al suo
primo elemento, di fatto, ogni elemento di data
ne
contiene un adeguato riferimento ossia un puntatore a
int
.
Così, data[0]
conterrà l’indirizzo di memoria del
primo elemento del primo array letterale (elementi con valori
1
e 2
), data[1]
conterrà
l’indirizzo di memoria del primo elemento del secondo array
letterale (elementi con valori 3
, 4
e
5
) e così via per data[2]
e
data[3]
.
Per quanto riguarda la valutazione da parte del compilatore del
nome data
, da solo e con l’operatore di indirizzo
&
, abbiamo che:
data
corrisponde aint **
, ossia a un puntatore a un puntatore aint
. Esso ritorna l’indirizzo di memoria dell’elemento 0 (0x0028fe90
) che è in pratica visualizzabile come il primo puntatore all’array contenente i valori1
e2
;&data[0]
corrisponde aint **
, ossia a un puntatore a un puntatore aint
. Esso ritorna l’indirizzo di memoria dell’elemento 0 (0x0028fe90
) che è in pratica visualizzabile come il primo puntatore all’array contenente i valori1
e2
;data[0]
corrisponde aint *
, ossia a un puntatore a unint
. Esso ritorna l’indirizzo di memoria riferito dall’elemento 0 (0x0028fea0
) che è in pratica visualizzabile come il primo elemento dell’array puntato (valore1
);&data[0][0]
corrisponde aint *
, ossia a un puntatore aint
. Esso ritorna l’indirizzo di memoria (0x0028fea0
) dell’elemento 0 del primo array riferito (valore1
);&data
corrisponde aint *(*)[4]
, ossia a un puntatore a un array di 4 elementi a puntatori aint
. Esso ritorna l’indirizzo di memoria (0x0028fe90
) che è in pratica quello a partire dal quale inizia tutto l’array di puntatori a int.
Invece, per la valutazione di data
con l’operatore
di deriferimento *
abbiamo che:
*data
corrisponde aint *
, ossia a un puntatore a unint
. Esso ritorna l’indirizzo di memoria riferito dall’elemento 0 (0x0028fea0
) che è in pratica visualizzabile come il primo elemento dell’array puntato (valore1
);*&data[0]
corrisponde aint *
, ossia a un puntatore a unint
. Esso ritorna l’indirizzo di memoria riferito dall’elemento 0 (0x0028fea0
) che è in pratica visualizzabile come il primo elemento dell’array puntato (valore1
);*data[0]
corrisponde a unint
. Esso ritorna il valore dell’elemento posto alla colonna 0 e riga 0 del primo array riferito, ossia1
;*&data[0][0]
corrisponde a unint
. Esso ritorna il valore dell’elemento posto alla colonna 0 e riga 0 del primo array riferito, ossia1
;*&data
corrisponde aint **
, ossia a un puntatore a un puntatore diint
. Esso ritorna l’indirizzo di memoria dell’elemento 0 (0x0028fe90
) che è in pratica visualizzabile come il primo puntatore all’array contenente i valori1
e2
.
Infine, utilizzando in modo assoluto la notazione puntatore/offset in alternativa a quella propria degli array, avremo che:
data + r
sposta il puntatore alla riga indicata dar
e ritorna un tipoint **
. Per esempio,data + 1
sposterà il puntatore alla riga1
, ossia all’indirizzo0x0028fe94
. Ciò avviene perchédata
è un puntatore a un puntatore diint
; pertanto aggiungere un’unità di storage a esso ne farà incrementare l’indirizzo di 4 byte, perché tale dimensione, sul sistema in uso, è quella usata per allocare un puntatore a unint
;
DETTAGLIO
In sostanza in questo caso, rispetto al puntatore
data
di int (*data)[4]
, il nome
data
è l’indirizzo di memoria del primo puntatore a
int
allocato che contiene un puntamento verso il primo
array (quello con i valori 1
e 2
). Quindi
l’incremento di una unità farà spostare il puntamento corrente al
secondo puntatore a int
che contiene un puntamento
verso il secondo array (quello con i valori 3
,
4
e 5
). Lo spostamento sarà di 4 byte
alla volta perché con il sistema corrente il compilatore userà 4
byte per allocare un tipo puntatore a int
. La Figura
7.15 evidenzia, per esempio, come &data[0]
sia il
primo puntatore allocato all’indirizzo 0x0028fe90
(l’elemento 0 dell’array data
), che contiene come
valore l’indirizzo 0x0028fea0
, che è l’area di memoria
a partire da cui si troveranno gli elementi dell’array riferito (in
pratica la prima riga dell’array data
).
*(data + r)
sposta il puntatore alla riga indicata dar
e poi la dereferenzia ritornando un tipoint *
. Per esempio,*(data + 1)
sposterà il puntatore alla riga 1 e tornerà un riferimento al suo primo elemento posto all’indirizzo0x0028fea8
;*(data + r) + c
sposta il puntatore alla riga indicata dar
e poi la dereferenzia ritornando l’indirizzo di memoria dell’elemento 0 come tipoint *
, che poi sposta dic
unità di storage. Per esempio,*(data + 1) + 1
sposterà il puntatore alla riga 1 e colonna 1, ossia all’indirizzo0x0028feac
. Ciò avviene perché il puntatore da spostare si riferisce a quello che “muove” le colonne dove ogni unità di storage vale 4 byte (ossia unint
di 32 bit sull’attuale sistema);*(*(data + r) + c)
sposta il puntatore alla riga indicata dar
e poi la dereferenzia ritornando l’indirizzo di memoria dell’elemento 0 come tipoint *
, che poi sposta dic
unità di storage. Infine, dereferenzia tale indirizzo di memoria ritornando un tipoint
. Per esempio*(*(data + 1) + 1)
, ritornerà il valore4
che è posto nella colonna 1 della riga 1.
Quanto mostrato, seppur complesso, si può sintetizzare dicendo che:
- per accedere a un determinato elemento della matrice, si può
utilizzare la più semplice notazione con indicizzazione propria
degli array (per esempio
data[1][1]
accede alla colonna 1 della riga 1); - per impiegare un parametro formale di tipo array bidimensionale
irregolare, si può dichiararlo con la consueta notazione
con indicizzazione degli array (per esempio void
foo(int *data[]) { … }
) oppure con la notazione che fa uso esclusivo dei puntatori (per esempiovoid foo(int **data) { … }
).
Listato 7.7 ArrayOfPointersAsParameter.c (ArrayOfPointersAsParameter).
/* ArrayOfPointersAsParameter.c :: Array di puntatori come parametri di una funzione per array 2d :: */
#include <stdio.h>
#include <stdlib.h>
#define ROWS 4
#define COLS_I 2
#define COLS_II 3
#define COLS_III 4
#define COLS_IV 5
// prototipo della funzione search
// equivalente --> int search(int **, int);
// --> int search(int *ptr_to_data[], int rows);
// --> int search(int **ptr_to_data, int rows);
int search(int *[], int);
int main(void)
{
// array di 4 puntatori a int
int *data[] =
{
(int[]) {1, 2}, // 2 colonne
(int[]) {3, -4, 5}, // 3 colonne
(int[]) {6, -7, 8, 9}, // 4 colonne
(int[]) {10, 11, -12, 13, -14} // 5 colonne
};
// invocazione di search
int res = search(data, ROWS);
printf("La matrice data contiene %d numeri negativi!\n", res);
return (EXIT_SUCCESS);
}
// definizione della funzione search
// equivalente --> int search(int **ptr_to_data, int rows) { ... }
int search(int *ptr_to_data[], int rows)
{
int nr = 0;
int cols_nr = 0;
// qui r++ fa spostare alla riga successiva della matrice perché
// ptr_to_data è di tipo int **
for (int r = 0; r < rows; r++)
{
switch(r) // necessario per sapere quante colonne ha la corrente riga
{
case 0: cols_nr = COLS_I; break;
case 1: cols_nr = COLS_II; break;
case 2: cols_nr = COLS_III; break;
case 3: cols_nr = COLS_IV; break;
}
// qui c++ fa spostare alla colonna successiva della riga corrente perché
// *(ptr_to_data + r) è di tipo int *
for (int c = 0; c < cols_nr; c++)
{
// sintassi alternativa puntatore/offset a quella propria degli array
int val = *(*(ptr_to_data + r) + c);
if (val < 0)
nr++;
}
}
return nr;
}
Output 7.7 Dal Listato 7.7 ArrayOfPointersAsParameter.c.
La matrice data contiene 4 numeri negativi!
Il Listato 7.7 è simile come logica al Listato 7.6, ossia data una matrice ne deve cercare gli elementi che contengono dei numeri negativi.
Tuttavia presenta le seguenti importanti differenze:
- la matrice
data
è dichiarata come array di puntatori aint
ed è irregolare, ossia ogni riga ha un diverso numero di colonne; - il prototipo e la definizione della funzione
search
hanno un parametro dichiarato, rispettivamente, comeint *[]
eint *ptr_to_data[]
(come detto, sarebbe preferibile scrivere i prototipi di funzione indicando anche gli identificatori dei tipi dei relativi parametri; tuttavia in taluni casi, come quello appena indicato, abbiamo ritenuto opportuno non farlo per ragioni di chiarezza); - dato che non è possibile sapere dinamicamente quante colonne ha
ciascuna riga [per esempio, un
sizeof(ptr_to_data)
quandoptr_to_data
si riferisce alla riga 0 darebbe come valore 4 e non 8 perché tanti sono i byte che occorrono per allocare un tipoint *
], siamo costretti a utilizzare un costruttoswitch
che a seconda della riga corrente valorizza la variabilecols_nr
con la costante simbolica relativa (per esempio, ser
vale0
alloracols_nr
conterrà il valore diCOLS_I
e così via per gli altri valori dir
).
Puntatori e array di lunghezza variabile
Un puntatore può riferire anche un array, sia monodimensionale sia multidimensionale, dichiarato con una lunghezza variabile (VLA, variable-length array).
Snippet 7.14 Puntatori a VLA.
// puntatore che riferisce un array monodimensionale
int nr_of_el = 5;
int data[nr_of_el];
int *ptr_to_data = data;
// puntatore che riferisce un array bidimensionale (un puntatore a un array di 5 colonne)
int nr_of_rows = 5;
int nr_of_cols = 5;
int data_m[nr_of_rows][nr_of_cols];
int (*ptr_to_data_m)[nr_of_cols] = data_m;
// puntatore che riferisce un array bidimensionale (un array di 5 puntatori a int)
int *data_o_m[nr_of_rows];
int **ptr_to_data_o_m = data_o_m;
I puntatori dichiarati nello Snippet 7.14 sono definiti dallo
standard C11 come variably modified types perché il loro
tipo dipende da valori non costanti: dal valore della variabile
nr_of_el
, nel caso di ptr_to_data
, e dal
valore della variabile nr_of_rows
, nel caso di
ptr_to_data_m
e ptr_to_data_o_m
.
TERMINOLOGIA
Un variably modified (VM) type è nella sostanza un puntatore a un array a lunghezza variabile (VLA).
Questi puntatori hanno poi, al pari dei VLA, alcune restrizioni
come, per esempio: possono essere dichiarati solo nelle funzioni
(anche come parametri) o in qualsiasi blocco di codice oppure come
parametri formali nei prototipi delle funzioni; i relativi
identificatori devono essere degli identificatori ordinari
(non possono essere, per esempio, identificatori di puntatori a VLA
che sono membri di struct
o union
).
Infine, per essi, l’aritmetica dei puntatori è ben definita, si comporta cioè come se tali puntatori puntassero ad array non VLA.
Puntatori a puntatori
Un puntatore a puntatore, già incontrato nel corso della trattazione sugli array di puntatori, è, in linea più generale, una variabile che contiene un indirizzo di memoria di un’altra variabile la quale contiene, anch’essa, un indirizzo di memoria di un’altra variabile che contiene un valore di un tipo determinato (Snippet 7.15).
Snippet 7.15 Puntatore a puntatore.
// dichiarazione di un doppio puntatore...
int number = 100;
int *ptr_to_number = &number;
int **ptr_to_ptr_to_number = &ptr_to_number;
// un deriferimento
// ritorna come valore l'indirizzo di memoria contenuto in ptr_to_number
int *first_der = *ptr_to_ptr_to_number;
// doppio deriferimento
// ritorna come valore il numero 100 che è contenuto in number
int value = **ptr_to_ptr_to_number;
In sostanza lo Snippet 7.15 si può leggere come segue, considerando anche, per i primi tre punti, la Figura 7.16.
- Dichiariamo la variabile di tipo
int
number
contenente il valore100
che viene allocata all’indirizzo di memoria0x006ffb84
. - Dichiariamo la variabile di tipo puntatore a
int
ptr_to_number
contenente il valore0x006ffb84
(che è l’indirizzo di memoria dinumber
) che viene allocata all’indirizzo di memoria0x006ffb78
. - Dichiariamo la variabile di tipo puntatore a puntatore a
int
ptr_to_ptr_to_number
contenente il valore0x006ffb78
(che è l’indirizzo di memoria diptr_to_number
) che viene allocata all’indirizzo di memoria0x006ffb6c
. - Dichiariamo la variabile di tipo puntatore a
int
first_der
contenente il valore0x006ffb84
(che è l’indirizzo di memoria dinumber
). Essa contiene tale indirizzo perché l’operatore di deriferimento applicato sul nomeptr_to_ptr_to_number
fa ritornare il valore contenuto nell’indirizzo di memoria0x006ffb78
che è, per l’appunto,0x006ffb84
. - Dichiariamo la variabile di tipo
int
value
contenente il valore100
. Essa contiene tale valore perché il primo operatore di deriferimento applicato sul nomeptr_to_ptr_to_number
fa ritornare il valore0x006ffb84
(che è l’indirizzo di memoria dinumber
), e poi l’altro operatore di deriferimento applicato su tale indirizzo fa ritornare, per l’appunto, il valore100
.
Vediamo ora un pratico esempio di utilizzo di un doppio puntatore (Listato 7.7) che consente di “simulare” un passaggio per riferimento di un argomento a un parametro di un funzione (ricordiamo che in C gli argomenti sono passati sempre per valore), in modo che venga modificato l’argomento stesso piuttosto che il valore da esso riferito (per effetto di ciò il parametro della funzione può essere considerato una sorta di alias dell’argomento, ossia un nome alternativo cui riferirlo e attraverso cui manipolarlo).
Listato 7.8 SimulatingPassByReference.c (SimulatingPassByReference).
/* SimulatingPassByReference.c :: Pass by reference con i doppi puntatori :: */
#include <stdio.h>
#include <stdlib.h>
void foo(int **p);
int main(void)
{
int a = 10;
int *j = &a;
// per stampare 0x si sarebbe potuto usare anche %#p
printf("Indirizzo riferito da j [ 0x%p ] PRIMA del passaggio dell'argomento\n", j);
foo(&j); // passo l'indirizzo di memoria di j esso stesso puntatore...
// per stampare 0x si sarebbe potuto usare anche %#p
printf("Indirizzo riferito da j [ 0x%p ] DOPO il passaggio dell'argomento e "
"*p = &k;\n", j);
return (EXIT_SUCCESS);
}
void foo(int **p)
{
static int k = 100;
*p = &k; // j è interessato... simulazione del pass by reference
}
Output 7.8 Dal Listato 7.8 SimulatingPassByReference.c.
Indirizzo riferito da j [ 0x0037fed8 ] PRIMA del passaggio dell'argomento
Indirizzo riferito da j [ 0x003a901c ] DOPO il passaggio dell'argomento e *p = &k;
Nel Listato 7.8 la funzione main
dichiara la
variabile a
di tipo int
e poi la
variabile j
come puntatore a essa. Stampa quindi
l’indirizzo di memoria contenuto in j
(0x0037fed8
) che è quello dove la variabile
a
è stata allocata. Invoca poi la funzione
foo
alla quale passa l’indirizzo di memoria dove è
stato allocato il puntatore j
medesimo
(0x0037fecc
).
A questo punto la funzione foo
, che prende il
controllo dell’esecuzione del codice, definisce la variabile locale
statica k
e poi ne assegna l’indirizzo di
memoria (0x003a901c
) a ciò cui punta p
,
ossia al puntatore j
, che d’ora in poi punterà a
questa nuova variabile piuttosto che a quella originaria
a
.
TERMINOLOGIA
Una variabile locale a una funzione si definisce
statica quando conserva il suo valore anche se il flusso
di esecuzione del codice esce dal blocco ove è dichiarata. Per
definire una variabile come statica si deve usare lo specificatore
di classe di memorizzazione espresso tramite la keyword
static
. Ritorneremo in modo approfondito su questo
punto nel Capitolo 9.
NOTA
Nella funzione foo
si è reso
necessario dichiarare la variabile k
come statica
perché non viene distrutta al termine dell’esecuzione della
funzione, e dunque il suo indirizzo di memoria, riferito dal
puntatore j
dopo l’assegnamento a esso compiuto per
mezzo dell’istruzione *p = &k;
, resta ancora
valido (non è utilizzato per allocare altre variabili) così come il
valore lì contenuto.
Quando, infine, il flusso di esecuzione del codice ritorna nella
funzione main
, la successiva istruzione
printf
stampa nuovamente il valore dell’indirizzo di
memoria riferito dal puntatore j
(0x003a901c
) che, questa volta, è quello non più alla
variabile a
, ma quello della variabile
k
.
Dunque, per il tramite del parametro p
della
funzione foo
è stato possibile cambiare l’argomento
stesso riferito grazie a un’implementazione “manuale” del
meccanismo del passaggio degli argomenti by reference non presente,
di “serie”, nel linguaggio C.
La Figura 7.17 dà una panoramica visuale di quanto descritto, sia dal punto di vista dei puntamenti sia della disposizione in memoria delle variabili utilizzate.
In sostanza, dall’analisi della disposizione in memoria della
variabili mostrata nella Figura 7.17 abbiamo che, prima
dell’assegnamento di *p = &k
nella funzione
foo
:
- la variabile
a
di tipoint
è allocata all’indirizzo di memoria0x0037fed8
; - la variabile
j
di tipo puntatore aint
è allocata all’indirizzo di memoria0x0037fecc
e contiene come valore l’indirizzo dia
, ossia0x0037fed8
; - la variabile
p
di tipo puntatore a puntatore aint
è allocata all’indirizzo di memoria0x0037fdf8
e contiene come valore l’indirizzo di memoria dij
, ossia0x0037fecc
; - la variabile
k
di tipo puntatore aint
è allocata all’indirizzo di memoria0x003a901c
.
Al termine delle operazioni avremo che p
conterrà
un riferimento a j
che conterrà un riferimento ad
a
che conterrà il valore 10
.
Dopo l’assegnamento di *p = &k
nella funzione
foo
abbiamo, invece, la seguente disposizione in
memoria delle stesse variabili:
- la variabile
a
di tipoint
continua a essere allocata all’indirizzo di memoria0x0037fed8
; - la variabile
j
di tipo puntatore aint
continua a essere allocata all’indirizzo di memoria0x0037fecc
, ma ora contiene come valore l’indirizzo dik
ossia0x003a901c
; - la variabile
p
di tipo puntatore a puntatore aint
continua a essere allocata all’indirizzo di memoria0x0037fdf8
e a contenere come valore l’indirizzo di memoria dij
, ossia0x0037fecc
; - la variabile
k
di tipo puntatore aint
continua a essere allocata all’indirizzo di memoria0x003a901c
.
Cos’è accaduto, quindi, di diverso dopo l’esecuzione
dell’istruzione *p = &k;
? In pratica
&k
dice al compilatore di fornire l’indirizzo di
memoria della variabile k
(0x003a901c
) e
di assegnarlo come contenuto all’indirizzo di memoria contenuto in
p
(0x0037fecc
) opportunamente
dereferenziato. Tale indirizzo di memoria, appartenendo a
j
, conterrà un nuovo indirizzo di puntamento, non più
verso la variabile a
ma verso la variabile
k
. Al termine delle operazioni avremo che
p
conterrà un riferimento a j
che
conterrà un riferimento a k
che conterrà il valore
100
.
Puntatori a funzione
C, da quello straordinario linguaggio che è, consente di utilizzare i puntatori anche per memorizzare in essi un indirizzo di memoria di una funzione, ossia di “codice” piuttosto che di “dati”, come è il caso dei puntatori sin qui analizzati.
Quanto detto non deve sorprendere: se un puntatore è una variabile deputata a contenere come valore un indirizzo di memoria allora, dal “suo punto di vista”, non fa alcuna differenza se tale indirizzo è quello utilizzato per allocare una variabile oppure per caricare del codice di una funzione (l’indirizzo di memoria di una funzione è quel punto della memoria a partire dal quale inizia il codice eseguibile della funzione stessa).
Sintassi 7.8 Dichiarazione di un puntatore a funzione.
data_type (*fptr_identifier)(void | parameters_list);
La Sintassi 7.8 evidenzia che la dichiarazione di un puntatore a
funzione è espressa scrivendola come un normale prototipo di
funzione, ma con la differenza che l’identificatore della funzione
deve essere preceduto dal carattere asterisco *
e
racchiuso tra una coppia di parentesi tonde ( )
.
Snippet 7.16 Dichiarazione di un puntatore a funzione.
int (*ptr_to_func)(int, int);
Lo Snippet 7.16 dichiara un puntatore a funzione,
ptr_to_func
, capace di contenere un indirizzo di
memoria di una qualsiasi funzione che ha la sua stessa
segnatura, ossia è del suo stesso tipo: ritorna un
int
e accetta due parametri di tipo
int
.
Le parentesi tonde che racchiudono il nome del tipo sono
essenziali perché permettono di identificare
ptr_to_func
come un puntatore a una funzione di tipo
(int, int) -> int
. In loro assenza, infatti,
int *ptr_to_func(int, int);
significherebbe
tutt’altro, e cioè che ptr_to_func
è una funzione che
ritorna un puntatore a un int
e ha due parametri di
tipo int
, e quindi la sua segnatura sarebbe
(int, int) -> int *
.
Dopo aver dichiarato un puntatore a funzione di un determinato tipo è possibile passare a esso l’indirizzo di memoria di una funzione dello stesso tipo utilizzando il consueto operatore di assegnamento, dove l’operando di sinistra sarà l’identificatore del puntatore a funzione mentre l’operando di destra sarà l’identificatore della funzione scelta (Snippet 7.17).
Snippet 7.17 Assegnamenti validi e non validi a un puntatore a funzione.
...
// vari prototipi di funzione…
int sub_P(int a, int b);
double sqrt_P(double j);
int main(void)
{
// puntatore a funzione di tipo (int, int) -> int
int (*ptr_to_func)(int, int);
ptr_to_func = sub_P; // ok, stesso tipo
ptr_to_func = sqrt_P; // no, non dello stesso tipo
ptr_to_func = sub_P(1,2); // no, nessun indirizzo ritornato
...
}
Nello Snippet 7.17 il puntatore a funzione
ptr_to_func
è inizializzato con tre valori differenti,
ma solo il primo assegnamento è valido e dunque corretto perché il
nome sub_P
si riferisce all’indirizzo di memoria di
una funzione del suo stesso tipo.
Il secondo assegnamento non è valido perché il nome
sqrt_P
si riferisce all’indirizzo di memoria di una
funzione di un tipo diverso (double) -> double
,
mentre il terzo assegnamento è ancora non valido perché il valore
assegnato è di tipo int
, ossia quello ritornato
dall’invocazione della funzione sub
con i valori
1
e 2
.
Nel secondo e terzo caso un compilatore come GCC ritornerà,
nell’ordine, i messaggi di warning assignment from
incompatible pointer type
e assignment makes pointer
from integer without a cast
.
A parte la validità o meno degli assegnamenti mostrati, è anche
importante comprendere che quando il compilatore incontra un
identificatore di una funzione, se è sprovvisto delle parentesi
tonde, allora lo valuta ritornando un puntatore, ovvero l’indirizzo
di memoria dove inizia il suo codice eseguibile (non è necessario
usare l’operatore di indirizzo &
). Se invece è
seguito dalla parentesi tonde, allora le stesse ne rappresentano
l’operatore di invocazione di funzione ed è generato l’opportuno
codice di chiamata della funzione stessa.
Per quanto attiene alla modalità di utilizzo di un puntatore a funzione, ossia a come sia possibile invocare la funzione riferita possiamo fare quanto segue (Snippet 7.18).
Snippet 7.18 Utilizzo di un puntatore a funzione.
...
// vari prototipi di funzione…
int sub_P(int a, int b);
double sqrt_P(double j);
int main(void)
{
// puntatore a funzione di tipo (int, int) -> int
int (*ptr_to_func)(int, int);
ptr_to_func = sub_P; // ok, stesso tipo...
int res = (*ptr_to_func)(100, 100); // ...risultato corretto
ptr_to_func = sqrt_P; // no, non dello stesso tipo...
double res_2 = (*ptr_to_func)(100, 100); // ...comportamento non definito
ptr_to_func = sub_P(1, 2); // no, nessun indirizzo ritornato...
int res_3 = (*ptr_to_func)(100, 100); // ...comportamento non definito
}
In definitiva è sufficiente usare le parentesi tonde al cui
interno dereferenziare il relativo puntatore al fine di far
ritornare l’indirizzo di memoria della funzione da invocare con i
seguenti argomenti. Per esempio, quando ptr_to_func
si
riferirà a sub_P
, scrivere (*ptr_to_func)(100,
100)
ne farà ritornare l’indirizzo, e dunque l’operatore di
invocazione di funzione con gli argomenti 100
e
100
sarà utilizzato su di esso (sarà, in pratica,
invocata sub_P
per il tramite di
ptr_to_func
e infatti da questo punto di vista
*ptr_to_func
è considerabile un alias di
sub_P
).
ATTENZIONE
Quando un puntatore di funzione contiene un riferimento verso un indirizzo di memoria di una funzione di un tipo differente il comportamento del compilatore sarà non definito.
NOTA
È possibile invocare una funzione riferita
tramite un puntatore a funzione utilizzando anche la forma
ptr_to_func(100, 100)
, ossia senza usare le parentesi
tonde ( )
e l’operatore di deriferimento
*
; questo perché quando ptr_to_func
è
valutato ritorna l’indirizzo di memoria di una funzione
direttamente chiamabile (ne è fatto un implicito deriferimento). In
ogni caso, anche se più prolissa, la prima forma, cioè
(*ptr_to_func)(100,100)
, rende più esplicito che
ptr_to_func
è un puntatore a funzione ed è per il suo
tramite che si sta invocando un’altra funzione. Se, infatti, si
utilizzasse un identificatore non significativo, per esempio
func
, si potrebbe pensare che func
sia
“direttamente” il nome della funzione che si sta invocando.
I puntatori a funzione sono un utile strumento di programmazione impiegato spesso per scrivere algoritmi o funzionalità “generiche”, dove cioè sia possibile separare il “cosa” l’algoritmo deve fare da il “come” lo deve fare effettivamente.
Il “come”, in C, è codificato in un’apposita funzione che viene poi fornita come argomento (il suo puntatore) al parametro (di tipo puntatore a funzione) di un’altra funzione, che ne rappresenta il “cosa”.
Per comprendere quanto asserito è possibile riferirci a un
classico esempio didattico che fa uso della funzione
qsort
, dichiarata nel file header
<stdlib.h>
della libreria standard del
linguaggio C con prototipo void qsort(void *base,
size_t nmemb,
size_t size, int (*compar)(const
void *, const void *))
, il cui obiettivo computazionale è
quello di ordinare gli elementi di un array in base a un
determinato criterio.
Analizzando il suo prototipo si nota subito come tale funzione sia in effetti “generica” perché sarà il client utilizzatore che dovrà fornirgli, come ultimo argomento, un puntatore a una funzione di comparazione che deciderà come gli elementi dell’array dovranno essere ordinati, ossia secondo quale modalità un elemento dovrà essere considerato minore, maggiore oppure uguale rispetto a un altro elemento.
In definitiva, la funzione qsort
dice “cosa” sta
eseguendo in quel momento, cioè che sta ordinando gli elementi di
un array; il “come” debbano essere ordinati, però, è espresso
tramite un’altra funzione che le viene passata come argomento.
TERMINOLOGIA
Una funzione che può accettare come suo argomento un’altra funzione e/o restituire una funzione come risultato della sua computazione è sovente indicata con il termine di higher-order function (funzione di ordine superiore).
Listato 7.9 PointersToFunctions.c (PointersToFunctions).
/* PointersToFunctions.c :: Puntatori a funzioni :: */
#include <stdio.h>
#include <stdlib.h>
// prototipi di funzione
// notare come gli identificatori dei parametri abbiano un nome diverso da quello
// dei corrispettivi identificatori indicati nella definizione di tali funzioni;
// ricordiamo che ciò non rappresenta alcun problema: sono semplicemente ignorati!
int makeOperations(int a, int b, int (*f)(int, int));
int addition(int a, int b);
int subtraction(int a, int b);
int multiplication(int a, int b);
int division(int a, int b);
int main(void)
{
int val1, val2, op = 0;
// array di puntatori a funzioni di tipo (int, int) -> int
int (*array_of_op[])(int, int) = {addition, subtraction, multiplication, division};
// array di puntatori a caratteri
char *op_name[] = {"addizione", "sottrazione", "moltiplicazione", "divisione"};
printf("***************** Operazioni Aritmetiche ***************************\n\n");
printf("[0] addizione\n[1] sottrazione\n[2] moltiplicazione\n[3] divisione\n\n");
scanf("%d", &op);
while (op < 0 || op > 3)
{
printf("Operazione aritmetica\n");
printf("[0] addizione, [1] sottrazione, [2] moltiplicazione, [3], divisione ");
scanf("%d", &op);
}
printf("\nPrimo numero: ");
scanf("%d", &val1);
printf("Secondo numero: ");
scanf("%d", &val2);
// stampa il risultato
printf("\nLa %s tra %d e %d ha prodotto come risultato %d\n",
op_name[op], val1, val2, makeOperations(val1, val2, array_of_op[op]));
printf("********************************************************************\n\n");
return (EXIT_SUCCESS);
}
// definizioni delle funzioni
int makeOperations(int value_1, int value_2, int (*op)(int, int))
{
// esegue la funzione riferita; può essere qualsiasi funzione di tipo
// (int, int) -> int
return (*op)(value_1, value_2);
}
int addition(int v1, int v2)
{
return v1 + v2;
}
int subtraction(int v1, int v2)
{
return v1 - v2;
}
int multiplication(int v1, int v2)
{
return v1 * v2;
}
int division(int v1, int v2)
{
return v1 / v2;
}
Output 7.9 Dal Listato 7.9 PointersToFunctions.c.
***************** Operazioni Aritmetiche ***************************
[0] addizione
[1] sottrazione
[2] moltiplicazione
[3] divisione
2
Primo numero: 20
Secondo numero: 30
La moltiplicazione tra 20 e 30 ha prodotto come risultato 600
********************************************************************
Il Listato 7.9 mette in pratica quanto sin qui detto sulla possibilità di scrivere funzioni generiche che si avvalgono dei puntatori a funzioni; illustra anche un altro comune pattern di utilizzo degli stessi, ovvero quello che prevede la capacità di definire un array di puntatori a funzioni che consente di scegliere quale funzione invocare direttamente e in modo arbitrario tramite la comune e compatta notazione a indice propria degli array.
Nel programma, tutto ruota attorno alla funzione
makeOperations
che esegue una qualsiasi operazione tra
due valori numerici di tipo int
espressa tramite
un’apposita funzione di tipo (int, int) -> int
che
le viene passata come argomento.
NOTA
Nel prototipo di makeOperations
il
parametro puntatore a funzione può essere scritto anche senza
indicarne il nome, come in int (*)(int, int)
. Invece,
nella definizione di makeOperations
, il parametro
puntatore a funzione può essere scritto con la stessa sintassi
vista per la dichiarazione (Sintassi 7.8), come in int
(*op)(int, int)
.
La funzione makeOperations
permette, quindi, di
generalizzare cosa computa e di separare, nettamente il suo codice
dal codice delle funzioni che eseguono nella sostanza la relativa
computazione (come, cioè, essa è eseguita).
Questo aspetto è di notevole importanza perché consente di
evitare che all’interno della funzione makeOperations
si scriva anche il codice delle computazioni, cioè delle operazioni
aritmetiche; se, infatti, una qualsiasi delle funzioni di
computazione viene in seguito cambiata (si pensi alla funzione
division
che nell’implementazione corrente non prevede
il check della possibile divisione per 0
), tale
cambiamento non ha alcun impatto su makeOperations
,
che continua a funzionare in modo trasparente.
La funzione main
, invece, definisce un menu di
scelta delle quattro operazioni aritmetiche fondamentali
memorizzandone il risultato nella variabile op
, la
quale verrà poi utilizzata per “estrarre” dall’array
array_op
e op_name
, rispettivamente, la
funzione dell’operazione da invocare e una stringa di caratteri che
ne dà il nome significativo.
In merito all’identificatore array_op
, esso
dimostra come dichiarare un array di puntatori a funzioni di un
certo tipo; nel nostro caso è un array di puntatori a funzioni che
ritornano un int
e accettano come argomenti due
int
, e pertanto si presta bene a contenere come
elementi i puntatori alle funzioni addition
,
subtraction
, multiplication
e
division
.
L’identificatore op_name
, invece, è un array dove
ogni elemento è un puntatore a un carattere, ossia a un indirizzo
di memoria a partire dal quale si trovano, in successione, tutti
gli altri caratteri della stringa corrispondente.
Dopo la scelta dell’operazione da eseguire, memorizziamo i
valori da computare nelle variabili val1
e
val2
ed eseguiamo la funzione printf
dove
forniamo come argomenti, nell’ordine: un stringa che descrive
l’operazione scelta; il primo valore da computare; il secondo
valore da computare; il risultato dell’operazione relativa.
Il risultato dell’operazione è ottenuto tramite l’espressione
makeOperations(val1, val2, array_of_op[op])
. Data la
sua importanza didattica, appare opportuno scomporre nei seguenti
passi per comprenderla pienamente.
- Viene invocata la funzione
makeOperations
alla quale si passanoval1
,val2
e poi il puntatore alla funzione che rappresenta l’elemento dell’arrayarray_of_op
posizionato all’indiceop
. Se per esempioop
vale1
, alloraarray_of_op[1]
ritornerà il puntatore alla funzionesubtraction
. - Viene eseguita la funzione
makeOperations
, che dereferenzia il suo parametroop
in modo da ottenere la locazione di memoria della funzione da eseguire alla quale passa come valorivalue_1
evalue_2
. Al termine dell’esecuzione della funzione puntata ne ritorna il risultato al chiamante. Ritornando al nostro esempio del punto 1,*op
rappresenterà la funzionesubtraction
che sarà eseguita e che ritornerà come risultato la differenza tra i valori forniti come argomenti, che sarà a sua volta ritornata almain
damakeOperations
.
Un altro esempio molto comune di utilizzo dei puntatori a funzione si ha quando si desidera impostare delle funzioni listener o di callback, ossia delle funzioni che devono essere invocate all’accadimento di un determinato evento.
Per implementare tale meccanismo si può definire una funzione,
diciamo foo
, che avrà come parametro un puntatore a
funzione (che riferirà la funzione di callback) la quale, allo
scatenarsi dell’evento desiderato, per il “tramite” di
foo
, verrà invocata.
NOTA
Il meccanismo delle funzioni di callback è
sovente usato per assegnare delle funzioni handler come
argomenti ad altre funzioni che rappresentano degli eventi che
possono accadere sui componenti o widget delle interfacce grafiche.
Così, potremmo definire una funzione onclick
per un
pulsante alla quale poter assegnare un’altra funzione che
rappresenta il “comportamento” che dovrà essere intrapreso quando
l’utente farà clic sul pulsante relativo.
Listato 7.10 Callback.c (Callback).
/* Callback.c :: Puntatori a funzioni come callback:: */
#include <stdio.h>
#include <stdlib.h>
#include <time.h>
#define NR_OF_ELEMS 10
// prototipi di funzione
void done(int res); // viene invocata solo se il risultato della somma è positivo
void fail(int code_nr); // viene invocata solo se il risultato della somma è negativo
// primo parametro -> array di elementi da sommare
// secondo parametro -> funzione di callback da eseguire se la computazione è eseguita
// correttamente
// terzo parametro -> funzione di callback da eseguire se la computazione
// NON è eseguita correttamente
void sum(int elems[], void(*done)(int), void(*fail)(int));
int main(void)
{
// inizializzazione del generatore pseudo-casuale dei numeri
srand((unsigned int) time(NULL));
int elems[NR_OF_ELEMS];
// inizializzazione elementi dell'array
for (int i = 0; i < NR_OF_ELEMS; i++)
elems[i] = (rand() % 1001) + (-500); // tra +500 e -500
sum(elems, done, fail);
return (EXIT_SUCCESS);
}
// definizioni delle funzioni
void done(int res)
{
printf("Il risultato e' %d\n", res);
}
void fail(int code_nr)
{
printf("Attenzione errore %d di computazione [ risultato < 0 ]\n", code_nr);
}
void sum(int elems[], void(*done)(int), void(*fail)(int))
{
int total = 0;
for (int ix = 0; ix < NR_OF_ELEMS; ix++)
total += elems[ix];
if (total >= 0)
(*done)(total); // chiamo la callback riferita dal parametro done
else
(*fail)(-1); // chiamo la callback riferita dal parametro fail
}
Output 7.10 Dal Listato 7.10 Callback.c.
Attenzione errore -1 di computazione [ risultato < 0 ]
Il Listato 7.10 mostra un semplice esempio di implementazione di
un meccanismo di callback dove la funzione sum
è
definita in modo che, all’accadimento dell’evento di completamento
della somma degli elementi di un array passato come primo
argomento, se il risultato della computazione è positivo, allora
verrà invocata la funzione done
passata come secondo
argomento altrimenti, se il risultato è negativo, come è il caso
mostrato dall’Output 7.10, verrà invocata la funzione
fail
passata come terzo argomento.
typedef e puntatori a funzione
La sintassi di dichiarazione di un puntatore a
funzione è abbastanza elaborata e a volte la sua prolissità può
portare a scrivere codice poco leggibile. Al fine, quindi, di
rendere più chiaro il codice sorgente ma anche più agevole
l’utilizzo di un puntatore a funzione, è possibile utilizzare la
keyword typedef
. Per esempio, l’istruzione
typedef int (*ptr_to_func)(int, int)
crea l’alias
ptr_to_func
, che è un puntatore a una funzione che ha
due parametri di tipo int
e ritorna un tipo int. Poi,
nell’ambito del codice sorgente si potrà scrivere:
ptr_to_func myFunc
; per dichiarare myFunc
come un identificatore del tipo puntatore a funzione
ptr_to_func
, ossia del tipo puntatore a una funzione
di tipo (int, int) -> int
(Listato 7.11).
Listato 7.11 typedefForFunctionPointer.c (typedefForFunctionPointer).
/* typedefForFunctionPointer.c :: typedef e puntatori a funzioni :: */
#include <stdio.h>
#include <stdlib.h>
// typedef per una funzione di tipo (int, int) -> int
typedef int (*ptr_to_operations)(int, int);
int sum(int a, int b);
int sub(int a, int b);
// senza il typedef la dichiarazione di una funzione che ritorna un puntatore a funzione
// di tipo (int, int) -> int sarebbe stata molto più complessa e poco leggibile:
// int(*choose(char))(int, int);
ptr_to_operations choose(char);
// senza il typedef la dichiarazione di una funzione che accetta come argomento
// un puntatore a funzione di tipo (int, int) -> int sarebbe stata molto più complessa
// e poco leggibile:
// int makeComputation(int (*)(int, int), int, int);
int makeComputation(ptr_to_operations, int, int);
int main(void)
{
int value1 = 2000;
int value2 = 1000;
// eseguo prima l'addizione
printf("Addizione tra %d e %d = %d\n", value1, value2,
makeComputation(choose('+'), value1, value2));
// eseguo poi la sottrazione
printf("Sottrazione tra %d e %d = %d\n", value1, value2,
makeComputation(choose('-'), value1, value2));
return (EXIT_SUCCESS);
}
int sum(int n1, int n2)
{
return n1 + n2;
}
int sub(int n1, int n2)
{
return n1 - n2;
}
// senza il typedef la definizione di una funzione che ritorna un puntatore a funzione
// sarebbe stata molto più complessa e poco leggibile:
// int(*choose(char code))(int, int)
ptr_to_operations choose(char code)
{
switch (code)
{
case '+': return sum;
case '-': return sub;
}
// nessuna scelta valida, quindi ritorniamo un puntatore nullo a indicare nessun
// indirizzo di puntatore a funzione valido
return NULL;
}
// senza il typedef la dichiarazione di una funzione che accetta come argomento
// un puntatore a funzione sarebbe stata molto più complessa e poco leggibile:
// int makeComputation(int (*op)(int, int), int n1, int n2)
int makeComputation(ptr_to_operations op, int n1, int n2)
{
// se ptr_to_operations contiene un indirizzo di puntatore a funzione valido
// esegui la funzione riferita; equivalente a *op != NULL
if (*op)
return (*op)(n1, n2);
else
{
printf("ATTENZIONE ptr_to_operations contiene un indirizzo non usabile!\n");
printf("ESCO subito dal programma!\n");
exit(EXIT_FAILURE);
}
}
Output 7.11 Dal Listato 7.11 typedefForFunctionPointer.c.
Addizione tra 2000 e 1000 = 3000
Sottrazione tra 2000 e 1000 = 1000
Puntatori a void
Dal punto di vista di un puntatore, un indirizzo di memoria non è altro che una locazione di storage a partire dalla quale viene memorizzato un valore di un determinato tipo.
Sinora abbiamo visto che ogni puntatore deve avere un tipo
associato affinché il compilatore possa essere in grado, quando si
applica l’operatore di deriferimento *
sul puntatore
medesimo, di interpretarlo in modo adeguato ed estrarne il valore
corretto.
In ogni caso, in C è possibile dichiarare anche puntatori al
tipo void
(void *
), ossia puntatori che
non puntano a nessun tipo in particolare, oppure, detto in altro
modo, puntatori che possono puntare a qualsiasi tipo (sono
definiti, infatti, puntatori generici).
Tuttavia, quando si utilizzano puntatori a void
è
importante considerare che su di essi non è possibile applicare
l’operatore di deriferimento *
, così come usare
l’aritmetica dei puntatori, perché, contenendo degli indirizzi di
memoria di tipi “sconosciuti”, il compilatore non è in grado di
sapere quanti byte deve usare per dereferenziarli
correttamente.
Però, a differenza dei puntatori a un qualsiasi tipo
T
, laddove se si assegna un puntatore a un tipo (per
esempio float *
) a un puntatore a un tipo diverso (per
esempio int *
) un compilatore emette un apposito
messaggio di diagnostica di incompatibilità di assegnamento, i
puntatori a tipi diversi possono sempre essere assegnati a
puntatori a void
e viceversa un puntatore a
void
può essere sempre assegnato a un puntatore a un
altro tipo.
ATTENZIONE
È possibile solo dichiarare variabili di tipo
void *
ossia puntatori a void
ma mai
variabili di tipo void
ossia variabili di tipo
void
.
Listato 7.12 VoidPointers.c (VoidPointers).
/* VoidPointers.c :: Puntatori a void :: */
#include <stdio.h>
#include <stdlib.h>
int main(void)
{
int i_data = 100;
double d_data = 223.2232;
int *ptr_to_int = &i_data;
double *ptr_to_double = &d_data;
// ATTENZIONE assegnamento tra puntatori a tipi diversi
ptr_to_int = ptr_to_double;
printf("Deriferimento di ptr_to_int che contiene l'indirizzo contenuto in "
"ptr_to_double\n[ %d ]\n", *ptr_to_int);
ptr_to_int = &i_data;
// puntatore a void
void *ptr_to_void = ptr_to_int; // ora punta a un int
// ptr_to_void è convertito a int * e poi dereferenziato
printf("Deriferimento di ptr_to_void che contiene l'indirizzo contenuto in "
"ptr_to_int\n[ %d ]\n", *(int*) ptr_to_void);
ptr_to_void = ptr_to_double; // ora punta a un double
// ptr_to_void è ora convertito a double * e poi dereferenziato
printf("Deriferimento di ptr_to_void che contiene l'indirizzo contenuto in "
"ptr_to_double\n[ %.4f ]\n", *(double*) ptr_to_void);
return (EXIT_SUCCESS);
}
Output 7.12 Dal Listato 7.12 VoidPointers.c.
Deriferimento di ptr_to_int che contiene l'indirizzo contenuto in ptr_to_double
[ 1951633139 ]
Deriferimento di ptr_to_void che contiene l'indirizzo contenuto in ptr_to_int
[ 100 ]
Deriferimento di ptr_to_void che contiene l'indirizzo contenuto in ptr_to_double
[ 223.2232 ]
Il Listato 7.12 definisce alcune variabili e dei puntatori a
esse. Poi prova ad assegnare la variabile
ptr_to_double
, che è un puntatore a un
double
, alla variabile ptr_to_int
, che è
un puntatore a int
, facendo emettere dal compilatore
in uso (GCC) il messaggio warning: assignment from
incompatible pointer type
.
Proviamo, quindi, a far stampare mediante la funzione
printf
il valore puntato da ptr_to_int
che però, come dimostrato dall’Output 7.12, è incongruo; ciò si
verifica perché l’indirizzo di memoria contenuto nella variabile
ptr_to_int
è di una variabile di tipo
double
(d_data
), che è a sua volta
contenuto nella variabile ptr_to_double
. Pertanto il
compilatore quando userà l’operatore di deriferimento
*
su ptr_to_int
interpreterà i byte
dell’indirizzo come memoria contenente un valore intero e non,
invece, come memoria contenente un valore di tipo decimale
(leggerà, per esempio, solo i primi 4 byte propri di un
int
e non tutti gli 8 byte propri di un
double
sul corrente sistema target).
Infine, dimostriamo come l’assegnamento di un puntatore a un
tipo (per esempio int *
o double *
) a un
puntatore a void
(void *
) non faccia
generare alcun warning da parte del compilatore ma, anzi, la
relativa conversione con cast esplicito [(int *)
e poi
(double *)
], e poi l’operazione di deriferimento,
consentano di far accedere al valore corretto presente nel corrente
indirizzo di memoria riferito.
Solitamente, comunque, i puntatori a void
sono un
utile strumento per costruire funzioni generiche, ossia
funzioni che sono in grado di accettare argomenti di differente
tipo oppure di ritornare tipi differenti (Listato 7.13).
Listato 7.13 GenericFunctions.c (GenericFunctions).
/* GenericFunctions.c :: Funzioni generiche :: */
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
// prototipo di swap
void g_swap(void *val_1, void *val_2, size_t size);
int main(void)
{
int a = 10;
int b = 20;
float f = 10.20f;
float g = 22.33f;
printf("Valori di a e b prima dello swap\t[ %d ] [ %d] \n", a, b);
// swap di tipi int
g_swap(&a, &b, sizeof (int));
printf("Valori di a e b dopo lo swap\t\t[ %d ] [ %d] \n", a, b);
printf("Valori di f e g prima dello swap\t[ %.2f ] [ %.2f] \n", f, g);
// swap di tipi float
g_swap(&f, &g, sizeof (float));
printf("Valori di f e g dopo lo swap\t\t[ %.2f ] [ %.2f] \n", f, g);
return (EXIT_SUCCESS);
}
// definizione di swap
void g_swap(void *val_1, void *val_2, size_t size)
{
void *tmp = malloc(size);
memcpy(tmp, val_1, size);
memcpy(val_1, val_2, size);
memcpy(val_2, tmp, size);
free(tmp);
}
Output 7.13 Dal Listato 7.13 GenericFunctions.c.
Valori di a e b prima dello swap [ 10 ] [ 20]
Valori di a e b dopo lo swap [ 20 ] [ 10]
Valori di f e g prima dello swap [ 10.20 ] [ 22.33]
Valori di f e g dopo lo swap [ 22.33 ] [ 10.20]
Il Listato 7.13 definisce la funzione generica
g_swap
che consente di scambiare il valore di due
variabili di qualsiasi tipo. Essa è definita con due parametri
formali di tipo puntatore a void
e un terzo parametro
formale, di tipo size_t
, atto a contenere la
dimensione del tipo di dato da elaborare e che è fondamentale per
processare i valori dei tipi passati (un void *
è
considerabile come un puntatore a un blocco di memoria
grezzo, e non sa nulla in merito a cosa è un tipo di dato
o alla sua dimensione in byte).
Il body della funzione g_swap
dichiara, come primo
oggetto, la variabile tmp
come un tipo void
*
e le passa, grazie alla funzione malloc
,
l’indirizzo di memoria di partenza dello spazio di storage allocato
della dimensione fornita dal parametro size
.
Ciò permetterà di allocare e processare la giusta quantità di
memoria occorrente per dei tipi int
,
float
, double
e così via, passati come
argomenti alla funzione g_swap
.
Successivamente, utilizza la funzione memcpy
,
dichiarata nel file header <string.h>
, per
copiare il contenuto della memoria di una variabile sorgente (il
secondo argomento) nell’area di memoria riferita da una variabile
destinazione (il primo argomento):
- con la prima invocazione,
memcpy
copierà il contenuto della variabile riferita daval_1
nell’area di memoria riferita datmp
; - con la seconda invocazione,
memcpy
copierà il contenuto della variabile riferita daval_2
nell’area di memoria riferita daval_1
; - con la terza invocazione di
memcpy
, copierà il contenuto della memoria riferita datmp
nell’area di memoria riferita daval_2
.
Infine utilizza la funzione free
, dichiarata nel
file header <stdlib.h>
, per liberare la memoria
allocata e puntata dalla variabile tmp
.
Puntatori nulli
Un puntatore nullo (null pointer) è un puntatore che contiene un valore “speciale” atto a segnalare che esso non punta a niente, ossia non punta e non riferisce alcun indirizzo di memoria concretamente utilizzabile di alcun oggetto (dato) o funzione (codice).
Lo standard del linguaggio C stabilisce che un’espressione
costante intera con il valore 0
oppure la stessa
espressione convertita in un puntatore a void
(void *
) rappresenta una costante di tipo puntatore
nullo (null pointer constant), la quale genera un
puntatore nullo quando è utilizzata durante un’istruzione di
inizializzazione, assegnamento o comparazione con una variabile di
tipo puntatore.
Tale costante di tipo puntatore nullo è espressa attraverso la
macro NULL
, che è definita, in linea generale e dalla
maggior parte dei compilatori, come #define NULL ((void
*)0)
in molti file header come <stdio.h>
,
<stdlib.h>
, <stddef.h>
e così
via.
ATTENZIONE
Un puntatore nullo è differente da un puntatore non inizializzato. Il primo, infatti, non punta né a un oggetto né a una funzione; il secondo, invece, punta a qualsiasi cosa.
NOTA
Ogni compilatore è libero di scegliere la propria
rappresentazione di un puntatore nullo, e dunque questo non
necessariamente dovrà avere come riferimento un indirizzo di
memoria tipo 0x00000000
ma potrà anche contenere un
indirizzo di memoria non esistente. Pertanto, dal punto di
vista di un programmatore, è sufficiente utilizzare
NULL
o 0
per generare un puntatore nullo
e avere la certezza che esso non punterà a niente di validamente
utilizzabile.
Quando si vuole assegnare a un puntatore un puntatore nullo è
possibile, in modo intercambiabile, utilizzare il valore
0
oppure il valore NULL
.
Tuttavia, è solo in quel contesto che 0
e
NULL
sono equivalenti (entrambi rappresentano una
costante di tipo puntatore nullo). Per esempio, assegnare il valore
0
a una variabile di tipo int
significa
semplicemente che essa conterrà quel valore costante intero, mentre
assegnare alla stessa variabile il valore NULL
potrà
fare generare a un compilatore un messaggio di diagnostica come
initialization makes integer from pointer without a
cast
, che indicherà, per l’appunto, che si sta provando ad
assegnare direttamente un puntatore a void
a una
variabile di tipo intero.
CONSIGLIO
Per motivi si stile e chiarezza usare sempre la
costante NULL
per assegnare a un puntatore un
puntatore nullo.
I puntatori nulli si rilevano, dunque, essenziali quando si deve
verificare se un puntatore contiene un valido indirizzo di memoria
referenziabile (si pensi alla funzione malloc
che se
riesce ad allocare lo spazio di storage richiesto ritorna un
puntatore valido a esso altrimenti ritorna un puntatore nullo).
Infatti: in caso di verifica affermativa (ha un indirizzo di
memoria diverso da 0) la valutazione del puntatore ritornerà un
valore uguale a 1
(true); in caso di verifica
negativa (ha un indirizzo di memoria uguale a 0) la valutazione del
puntatore ritornerà un valore uguale a 0
(false) e permetterà di evitare deriferimenti che
potrebbero causare eventi disastrosi come il crash del programma
attualmente in esecuzione (in ogni caso, per lo standard di C,
dereferenziare un puntatore nullo causerà un comportamento
non definito, e un compilatore potrebbe anche non far terminare un
programma ma generare piuttosto un risultato inaspettato o non
prevedibile).
Snippet 7.19 Puntatore nullo.
int value = 200;
// p_value è impostata per puntare a un puntatore nullo
int *p_value = NULL;
// p_value conterrà un riferimento a un puntatore nullo
// sarà quindi valutata come uguale a 0
_Bool b_p = p_value; // false - b_p conterrà il valore 0
p_value = &value; // puntatore a value
// p_value conterrà un riferimento a un puntatore valido
// sarà quindi valutata come uguale a 1
b_p = p_value; // true - b_p conterrà il valore 1
// un test per verificare se il puntatore è nullo
int nr = 100;
int *p_nr = &nr;
int storage;
// solo se p_nr contiene un indirizzo valido, dereferenziarlo ponendo
// il valore dell'oggetto puntato nella variabile storage
if (p_nr) // equivalente ma prolisso: if(p_nr != NULL)
storage = *p_nr;
else
storage = 0;
La keyword const e i puntatori
Il qualificatore di tipo espresso tramite la keyword
const
può essere applicato anche a un puntatore che, a
seconda della sua collocazione, assumerà una determinata semantica
(Sintassi 7.9, 7.10 e 7.11):
Sintassi 7.9 Puntatore a costante – qualificatore const prima del tipo di dato.
const data_type *ptr_identifier;
La Sintassi 7.9 permette di dichiarare un puntatore che potrà contenere un indirizzo di memoria di un oggetto di un determinato tipo che sarà considerato costante.
Ciò implica che il puntatore potrà puntare a un qualsiasi altro
indirizzo di memoria, ma il valore dell’oggetto riferito non potrà
subire modifiche per effetto dell’applicazione dell’operatore di
deriferimento *
oppure dell’operatore di subscript con
un indice, i quali potranno quindi essere utilizzati solo per delle
operazioni di lettura.
NOTA
Un puntatore a costante potrà contenere validamente sia l’indirizzo di memoria di un tipo costante sia di un tipo non costante. Ciò implica che la non modificabilità del valore è significativa solo dal punto di vista del puntatore a costante. Infatti, se contiene un indirizzo di memoria di un dato non costante, quest’ultimo potrà ancora subire modifiche per il tramite del suo identificatore ma non per il tramite dell’identificatore del puntatore a costante.
Snippet 7.20 Puntatore a costante.
// array non costante
int data[] = {100, 200, 300};
// array costante
const int ro_data[] = {-1, -2, -3};
// puntatore a costante di tipo int
// assegnamento di un dato non costante
const int *ptr_1 = data;
*ptr_1 = 10; // error: assignment of read-only location '*ptr_1'
data[0] = -100; // OK data non è const
// puntatore a costante di tipo int
// assegnamento di un dato costante
const int *ptr_2 = ro_data;
*ptr_2 = 10; // error: assignment of read-only location '*ptr_2'
ro_data[0] = 1; // error: assignment of read-only location 'ro_data[0]
// ok i puntatori a costante possono puntare ad altri oggetti
int other = 2;
ptr_1 = ptr_2 = &other;
Sintassi 7.10 Puntatore costante – qualificatore const prima dell’identificatore.
data_type *const ptr_identifier;
La Sintassi 7.10 permette di dichiarare un puntatore che potrà contenere un indirizzo di memoria di un oggetto di un determinato tipo ma che non potrà puntare e riferire altri indirizzi di memoria di altri oggetti dello stesso tipo.
Ciò implica che il puntatore non potrà puntare a qualsiasi altro
indirizzo di memoria, ma il valore dell’oggetto riferito potrà
subire modifiche per effetto dell’applicazione dell’operatore di
deriferimento *
oppure dell’operatore di subscript con
un indice i quali potranno quindi essere utilizzati per operazioni
di scrittura e di lettura.
Snippet 7.21 Puntatore costante.
// array non costante
int data[] = {100, 200, 300};
// array costante
const int ro_data[] = {-1, -2, -3};
// puntatore costante a un int
// assegnamento di un dato non costante
int *const ptr_1 = data;
*ptr_1 = 10; // OK il puntatore non è un puntatore a costante
data[0] = -100; // OK data non è const
// puntatore costante a un int
// assegnamento di un dato costante
int *const ptr_2 = ro_data; // warning: initialization discards 'const' qualifier
// from pointer target type
*ptr_2 = 10; // modifica di un dato costante con *ptr_2; comportamento non definito
ro_data[0] = 1; // error: assignment of read-only location 'ro_data[0]
int other = 2;
ptr_1 = &other; // error: assignment of read-only variable 'ptr_1'
ptr_2 = &other; // error: assignment of read-only variable 'ptr_2'
Lo Snippet 7.21 dichiara le stesse variabili dello Snippet 7.20
ma fa un’importante modifica: i puntatori ptr_1
e
ptr_2
diventano puntatori costanti, e dunque, dopo
l’assegnamento, rispettivamente dell’indirizzo di memoria del primo
elemento di data
e dell’indirizzo di memoria del primo
elemento di ro_data
, non potranno puntare ad altri
oggetti come evidenziato dall’errore di compilazione che il
compilatore ha emesso quando abbiamo tentato di assegnare
l’indirizzo della variabile other
ai predetti
puntatori.
Notiamo anche che il compilatore ha emesso un warning quando
l’indirizzo del primo elemento di ro_data
è stato
assegnato al puntatore costante ptr_2
.
Questo è accaduto perché, come regola generale, lo standard di C
asserisce che se si prova a modificare un oggetto costante per il
tramite di un oggetto non costante il comportamento sarà non
definito; il nostro compilatore, dunque, ci mette in guardia che
quell’inizializzazione scarta il qualificatore const
dell’oggetto riferito dal puntatore ptr_2
(l’oggetto
riferito era infatti di tipo const int
) e pertanto su
di esso potranno avvenire modifiche per il tramite dell’oggetto non
costante *ptr_2
(è di tipo int
). Tuttavia
non è possibile esere certi se queste modifiche avverranno o meno
oppure se vi potranno essere effetti differenti (il comportamento è
cioè non definito; potrà accadere qualsiasi cosa).
Quello che bisogna comprendere è che la non modificabilità di un puntatore costante è riferita solo al fatto che esso non può puntare a un altro indirizzo di memoria ma può certamente cambiare il valore dell’oggetto riferito.
Infatti, una dichiarazione come int *const ptr_2
può essere letta in modo estensivo, partendo da destra verso
sinistra, come “ptr_2
è un identificatore costante di
un oggetto non costante che è un puntatore a un int
”;
dunque, in questo caso, quello che è costante, è il puntatore che
non può subire modifiche del valore lì contenuto una volta
assegnato (in questo caso, quindi, &ro_data[0]
e
ptr_2
non avranno corrispondenza di dichiarazioni
const
tra il dato che potrà essere riferito, che sarà
di tipo const int
, e l’oggetto che potrà essere
dereferenziato, che sarà di tipo int
).
Invece, una dichiarazione come const int *ptr_2
presente nello Snippet 7.20 può essere letta in modo dettagliato,
sempre da destra verso sinistra, come: “ptr_2
è un
identificatore non costante di un oggetto costante che è un
puntatore a un const int
”; dunque, in questo caso,
quello che è costante è il dato dereferenziato (in questo caso,
quindi, &ro_data[0]
e ptr_2
avranno
corrispondenza di dichiarazioni const
tra il dato che
potrà essere riferito, che sarà di tipo const int
, e
l’oggetto che potrà essere dereferenziato, che sarà di tipo
const int
).
Sintassi 7.11 Puntatore costante a costante – qualificatore const prima del tipo e prima dell’identificatore.
const data_type *const ptr_identifier;
La Sintassi 7.11 permette di dichiarare un puntatore che potrà contenere un indirizzo di memoria di un oggetto di un determinato tipo che sarà considerato costante e non potrà puntare ad altri indirizzi di memoria di altri oggetti dello stesso tipo.
Ciò implica che il puntatore non potrà puntare a qualsiasi altro
indirizzo di memoria, e il valore dell’oggetto riferito non potrà
subire modifiche per effetto dell’applicazione dell’operatore di
deriferimento *
oppure dell’operatore di subscript con
un indice, i quali potranno quindi essere utilizzati solo per delle
operazioni di lettura.
Snippet 7.22 Puntatore costante a costante.
// array non costante
int data[] = {100, 200, 300};
// array costante
const int ro_data[] = {-1, -2, -3};
// puntatore costante a costante di tipo int
// assegnamento di un dato non costante
const int *const ptr_1 = data;
*ptr_1 = 10; // error: assignment of read-only location '*ptr_1'
data[0] = -100; // OK data non è const
// puntatore costante a costante di tipo int
// assegnamento di un dato costante
const int *const ptr_2 = ro_data;
*ptr_2 = 10; // error: assignment of read-only location '*ptr_2'
ro_data[0] = 1; // error: assignment of read-only location 'ro_data[0]
int other = 2;
ptr_1 = &other; // error: assignment of read-only variable 'ptr_1'
ptr_2 = &other; // error: assignment of read-only variable 'ptr_2'
CONSIGLIO
Per leggere e comprendere correttamente le
dichiarazioni di puntatori con o senza l’uso del qualificatore
const
si può procedere nel seguente modo, partendo
dall’identificatore e poi procedendo con tutti gli altri elementi
posti alla sua sinistra; per esempio, int *p;
dichiara
p
come un puntatore a un int
(p as
pointer to int); const int *p;
dichiara
p
come un puntatore a una costante di tipo int (p
as pointer to const int); int *const p;
dichiara
p
come un puntatore costante a un int
(p as const pointer to int); const int *const
p;
dichiara p
come un puntatore costante a una
costante di tipo int
(p as const pointer to const
int).
La keyword const
viene sovente impiegata con i
parametri di una funzione di tipo puntatore per specificare che gli
stessi non potranno modificare i relativi argomenti.
È molto comune, per esempio, dichiarare una funzione che deve elaborare gli elementi di un array passato come argomento con un parametro che è un puntatore a costante del tipo degli elementi dell’array. Questa tecnica, di fatto, permette il raggiungimento di due scopi contemporaneamente: il primo è legato all’efficienza, perché nel parametro non sono copiati tutti gli elementi dell’array ma solo l’indirizzo di memoria del suo primo elemento; il secondo, invece, è legato alla sicurezza, perché gli elementi dell’array passato come argomento non potranno subire modifiche inattese per il tramite del parametro puntatore.
Snippet 7.23 Caso d’uso di un puntatore a costante di tipo int come parametro di una funzione.
...
int sumArray(const int *elems, int size)
{
int sum = 0;
for (int i = 0; i < size; i++)
{
// se un elemento è negativo lo voglio rendere positivo...
if (*elems < 0)
*elems = -(*elems); // error: assignment of read-only location '*elems'
sum += elems[i]; // ok elems è usato solo in lettura
}
return sum;
}
int main(void)
{
int data[] = {-1, -2, -3, -4, -5, -6, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10};
sumArray(data, sizeof data / sizeof (int));
...
}
Lo Snippet 7.23 definisce la funzione sumArray
,
dove esplicita che il parametro elems
è un puntatore a
costante di tipo int
, ossia per il tramite di esso non
sarà possibile modificare un qualsiasi oggetto riferito che, per il
nostro obiettivo didattico, sono tutti gli elementi dell’array
data
passato come argomento e processati nel relativo
ciclo for
.
Questa limitazione ha permesso di evitare che nel body di
sumArray
potessimo modificare il valore di un elemento
dell’array data
, quando negativo, in positivo; in
pratica abbiamo sia protetto da modifiche l’array originario sia
ottenuto un’adeguata performance, perché nel puntatore
elems
, quando sumArray
è stata invocata,
sono stati copiati solo 4 byte (nel sistema in uso a 32 bit), ossia
la dimensione dello spazio di storage richiesto per memorizzare un
puntatore che nel nostro caso è rappresentato dall’indirizzo di
memoria del primo elemento dell’array data
.
La keyword restrict e i puntatori
A partire dallo standard C99, è stato introdotto un nuovo
qualificatore di tipo espresso tramite la keyword
restrict
che è applicabile solo a un puntatore il
quale diventa, in conseguenza della sua applicazione, un puntatore
ristretto (restricted pointer).
Sintassi 7.12 Puntatore ristretto.
data_type *restrict ptr_identifier;
La Sintassi 7.12 illustra che la keyword restrict
deve essere posta prima dell’identificatore del puntatore; essa,
infatti, qualifica il puntatore come ristretto e non l’oggetto cui
punta. Prima di spiegare cos’è un puntatore ristretto, è opportuno
illustrare alcuni concetti preliminari che aiuteranno a comprendere
il perché della sua introduzione.
- Aliasing: situazione per cui due o più oggetti si riferiscono alla stessa locazione di memoria che può dunque essere manipolata, in modo equivalente, per il tramite dei predetti oggetti (Snippet 7.24). In pratica l’aliasing consente di riferire uno stesso oggetto mediante l’impiego di più nomi.
Snippet 7.24 Aliasing.
int i_value = 100;
// aliasing: i due puntatori si riferiscono allo stesso indirizzo in memoria
int *ptr_to_i_value_1 = &i_value;
int *ptr_to_i_value_2 = &i_value;
// *ptr_to_i_value_1 è anche un alias di i_value
// entrambi riferiscono la stessa area di memoria che è modificata con il valore 300
*ptr_to_i_value_1 = 300;
La possibilità nel linguaggio C di creare alias può però portare sia a sottili errori di programmazione difficili da scoprire se tali alias non sono stati pianificati correttamente (per esempio, in una funzione si modifica il valore di un oggetto puntato da un parametro di tipo puntatore ma tale modifica non doveva però accadere) sia a possibili restrizioni cui devono sottostare i compilatori che non possono compiere eventuali ottimizzazioni sul codice eseguibile prodotto (per esempio, se un indirizzo di memoria è riferito da più oggetti, il compilatore dovrà necessariamente, per ogni oggetto, compiere opportune operazioni di lettura e/o scrittura del relativo valore perché ciascun oggetto potrà avere compiuto con tale indirizzo delle manipolazioni; in caso contrario, però, se un solo oggetto riferisce un indirizzo di memoria un compilatore potrebbe memorizzarne il valore nelle veloci unità di memoria quali sono i registri). Dal punto di vista di un compilatore, pertanto, non viene fatta alcuna assunzione che l’aliasing non avvenga e pertanto non compie, nell’eventualità, alcuna ottimizzazione.
- Strict aliasing: regola per cui un compilatore assume
che due o più puntatori, di tipo differente non si riferiscono mai
alla stessa locazione di memoria. In questo caso, quindi, un
compilatore è in grado di compiere certe ottimizzazioni per rendere
il codice eseguibile più veloce ed efficiente. Questo, comunque, se
da una parte può migliorare l’efficienza del codice generato, può
portare a comportamenti non definiti se il programmatore vìola la
regola dello strict aliasing (GCC, per esempio, genera solo il
messaggio
warning: dereferencing type-punned pointer will break strict-aliasing rules
, se si accorge della sua violazione).
TERMINOLOGIA
Per type punning si intende una tecnica
mediante la quale è possibile “aggirare” il type system di un
linguaggio di programmazione al fine di far manipolare il valore di
un tipo come valore di un altro tipo. Ciò può avvenire, per
esempio, quando si fa il cast in un puntatore a int
(destinazione) da un puntatore a float
(sorgente) e se
ne manipola il valore tramite il puntatore di destinazione.
Snippet 7.25 Strict aliasing.
float *data[2];
// violazione di strict aliasing:
// puntatori di tipo diverso puntano alla stessa area di memoria
// warning: dereferencing type-punned pointer will break strict-aliasing rules
int *i = (int *) data;
// warning: dereferencing type-punned pointer will break strict-aliasing rules
short *s = (short *) data;
// manipolazione dell'area di memoria con due puntatori a tipi differenti
// qualsiasi risultato possibile perché il compilatore assume che il
// programmatore rispetti la regola dello strict aliasing e prova a fare
// delle ottimizzazioni
*i = 42;
s[0] = 0;
s[1] = 1;
NOTA
In GCC si possono usare durante la fase di
compilazione i seguenti flag: -fstrict-aliasing
per
consentire al compilatore di assumere che venga rispettata la
regola dello strict aliasing; -Wstrict-aliasing=2
per
far attivare dei warning se il codice di un programma vìola la
regola dello strict aliasing che il compilatore usa per compiere
delle ottimizzazioni; -O3
per far attivare tutte le
ottimizzazioni possibili. Ciò detto è possibile mandare in
esecuzione del codice sorgente contenente lo Snippet 7.25 con, per
esempio, il seguente comando: gcc -std=c11 -fstrict-aliasing
-Wstrict-aliasing=2 -O3 7.25.c -o 7.25
.
Come abbiamo detto, un compilatore assumerà sempre che l’aliasing tra oggetti sia potenzialmente effettuabile e pertanto non provvederà mai a compiere alcuna ottimizzazione sul codice al fine di renderlo più efficiente.
Ecco allora che entra in gioco l’utilità della keyword
restrict
: essa darà l’indicazione al compilatore che
il programmatore “si impegnerà” a non far mai puntare la stessa
area di memoria a due o più puntatori dello stesso tipo e,
pertanto, potrà tentare di eseguire qualsiasi ottimizzazione sul
codice che riterrà opportuno.
NOTA
Un compilatore può anche ignorare la keyword
restrict
e non compiere alcuna ottimizzazione.
Tuttavia, e questo è un punto di notevole importanza da ricordare, il compilatore “si fiderà” del programmatore e non verificherà la violazione di questa sorta di contratto stipulato con esso: ciò significa che se viene fatto un alias di un puntatore ristretto e poi per il tramite di tale alias viene modificato l’oggetto puntato, avremo un comportamento non definito.
Snippet 7.26 Puntatori ristretti.
int value = 100;
int *restrict ptr_to_value_1 = &value;
// ATTENZIONE *ptr_to_value_2 è un alias di *ptr_to_value_1
int *restrict ptr_to_value_2 = ptr_to_value_1;
// ciò potrà causare un comportamento non definito
*ptr_to_value_2 = 1000;
Conversioni e puntatori
Anche i tipi puntatore, così come gli altri tipi di oggetti, sono soggetti a delle regole di conversione quando utilizzati nelle comuni operazioni di assegnamento, inizializzazione o confronto con altri tipi di puntatori oppure altri tipi di oggetti o costanti.
La Tabella 7.1 ne dà un riepilogo dove: la colonna T.O.S. (tipo operando a sinistra) indica il tipo posto a sinistra dell’operatore usato; la colonna T. O. D. (tipo operando a destra) indica il tipo posto a destra dell’operatore usato; la colonna Risultato indica se l’operazione dà un risultato definito (ossia se è fattibile senza problemi), non definito oppure dipendente dalla corrente implementazione.
T. O. S. | T. O. D. | Risultato |
---|---|---|
puntatore a un tipo T | void * | Definito |
void * | puntatore a un tipo T | Definito |
puntatore a funzione di tipo T | void * | Comportamento non definito |
void * | puntatore a funzione di tipo T | Comportamento non definito |
puntatore a un tipo T | 0 | Definito |
puntatore a un tipo T | qualsiasi intero | Definito dall’implementazione |
puntatore a funzione di tipo T | 0 | Definito |
puntatore a funzione di tipo T | qualsiasi intero | Definito dall’implementazione |
qualsiasi intero | puntatore a un tipo T | Definito dall’implementazione1 |
qualsiasi intero | puntatore a funzione di tipo T | Definito dall’implementazione1 |
puntatore a un tipo T | puntatore a un tipo W | Definito2 |
puntatore a un tipo T | puntatore a un tipo T | Definito |
puntatore a funzione di tipo T | puntatore a funzione di tipo W | Definito3 |
puntatore a funzione di tipo T | puntatore a funzione di tipo T | Definito |
1 Se però il risultato della conversione non può essere rappresentato in un tipo intero, allora il comportamento sarà non definito. | ||
2 Se però il puntatore risultante non è correttamente allineato in memoria per il tipo referenziato allora, se riconvertito nuovamente, il risultato sarà non definito. Altrimenti, se l’allineamento è corretto e il puntatore è riconvertito, il puntatore risultante dovrà essere uguale al puntatore originario (Snippet 7.27). | ||
3 Se però il puntatore della funzione di tipo T è usato per invocare la funzione riferita di tipo W, che non è quindi del suo stesso tipo, il comportamento sarà non definito. In più, dato un puntatore a una funzione di tipo W convertito in un puntatore a una funzione di tipo T, se il puntatore alla funzione di tipo T viene convertito nuovamente nel puntatore alla funzione di tipo W, vi dovrà essere la garanzia che il puntatore risultante sarà uguale al puntatore a funzione originario, e dunque il codice invocato sarà quello della funzione corretta. |
Snippet 7.27 Eventuale problema di allineamento tra puntatori a tipi differenti.
char c = 'A';
// conversione tra un puntatore a un char e un puntatore a un int
// potrebbe esserci perdita di informazione...
int *ip = (int*) &c;
// riconversione del puntatore a un int nel puntatore a un char
// non è detto che *cp ritorni il carattere 'A'
// in alcune implementazioni cp potrebbe non essere uguale a &c
char *cp = (char*) ip;
// b = true o b = false a seconda dell'implementazione!
_Bool b = cp == &c;
TERMINOLOGIA
Lo standard di C11 fornisce le seguenti indicazioni terminologiche, laddove per behavior intende quel comportamento che un programma può intraprendere a seguito dell’utilizzo dei costrutti del linguaggio C e per implementation intende l’ambiente software (compilatore, linker, sistema di run-time e così via) utilizzato per una particolare piattaforma hardware.
Undefined behavior (comportamento non definito): indica il comportamento di un programma a seguito di codice non portabile o erroneo per cui lo standard non impone alcun requisito in particolare. Ciò significa che un programma: può ignorare la situazione producendo però risultati non prevedibili; può segnalare il problema durante la compilazione o esecuzione; può terminare bruscamente e così via. In pratica, poiché qualsiasi cosa può accadere, è buona norma evitare sempre di scrivere codice che può produrre comportamenti non definiti.
Unspecified behavior (comportamento non
specificato): indica che un programma può “scegliere” di
comportarsi sulla base di un set di comportamenti definiti dallo
standard. Per esempio, un comportamento non specificato si ha
sull’ordine di valutazione degli argomenti di una funzione laddove,
data una funzione invocata come foo(a, b)
, potrebbe
essere valutato prima l’argomento b
e poi l’argomento
a
o viceversa. Resta inteso che se gli argomenti di
una funzione producono side-effect, e pertanto l’ordine di
valutazione è importante, allora il codice eseguibile può produrre
bug non facilmente individuabili. Anche in questo caso, quindi, è
bene evitare di scrivere codice che può produrre comportamenti non
specificati.
Implementation-defined behavior (comportamento definito dall’implementazione): indica che un programma può intraprendere uno dei comportamenti non specificati ma l’implementazione ne documenta la scelta. In pratica, il comportamento di un programma è dipendente dalla corrente implementazione e può, dunque, variare da implementazione a implementazione. Se si devono scrivere programmi portabili è consigliabile evitare di scrivere codice dipendente da una specifica implementazione.
Snippet 7.28 Esempi di conversioni con void *.
...
int sum(int a, int b)
{
return a + b;
}
double foo(void)
{
return 1.0;
}
int main(void)
{
int a = 100;
int b = 200;
// puntatore a un tipo T
int *ptr_to_a = &a;
// puntatore a una funzione di tipo T
int(*ptr_to_f)(int, int) = sum;
// puntatore a un tipo void
void *v_ptr = &b;
// puntatore a un tipo T <---> void *
// DEFINITO
int *a_ptr = v_ptr;
// void * <---> puntatore a un tipo T
// DEFINITO
void *v2_ptr = ptr_to_a;
// puntatore a una funzione di tipo T <---> void *
// COMPORTAMENTO NON DEFINITO
ptr_to_f = v_ptr;
// void * <--- > puntatore a una funzione di tipo T
// COMPORTAMENTO NON DEFINITO
double(*ptr_to_f_2)(void) = foo;
void *v3_ptr = ptr_to_f_2;
...
}
NOTA
Se un puntatore a un tipo T
è
convertito in un puntatore a void
e poi il puntatore a
void
è convertito nuovamente nel puntatore al tipo
T
, dovrà essere garantito che non ci sarà alcuna
perdita di informazione (in pratica il puntatore risultante dalla
riconversione del puntatore a void
nel puntatore al
tipo T
dovrà essere uguale al puntatore
originario).
Snippet 7.29 Esempi di conversioni con tipi interi.
...
int sum(int a, int b)
{
return a + b;
}
double foo(void)
{
return 1.0;
}
int main(void)
{
int a = 100;
int b = 200;
// puntatore a un tipo T
int *ptr_to_a = &a;
// puntatore a una funzione di tipo T
int(*ptr_to_f)(int, int) = sum;
// puntatore a un tipo T <---> un int
// DEFINITO DALL'IMPLEMENTAZIONE
int *ptr_to_int = 0x22334455;
// un int <---> puntatore a un tipo T
// DEFINITO DALL'IMPLEMENTAZIONE
int any = ptr_to_a;
// puntatore a una funzione di tipo T <---> un int
// DEFINITO DALL'IMPLEMENTAZIONE
ptr_to_f = 0x66777788;
// un int <---> puntatore a una funzione di tipo T
// DEFINITO DALL'IMPLEMENTAZIONE
double(*ptr_to_f_2)(void) = foo;
int any_2 = ptr_to_f_2;
...
}
Snippet 7.30 Esempi di conversioni tra puntatori a tipi differenti.
int a = 100;
float c = 222.3f;
// puntatore a un tipo T
int *ptr_to_a = &a;
// puntatore a un tipo W
float *ptr_to_c = &c;
// puntatore a un tipo T <---> puntatore a un tipo W
// DEFINITO se allineamento in memoria corretto
ptr_to_a = ptr_to_c;
// ptr_to_a riconvertito nuovamente in ptr_to_c
// se allineamento in memoria corretto *ptr_to_c darà come valore 222.3
// ossia il suo valore in virgola mobile
ptr_to_c = ptr_to_a;
Snippet 7.31 Esempi di conversioni tra puntatori a funzione di tipi differenti.
...
int sum(int a, int b)
{
return a + b;
}
double foo(void)
{
return 1.0;
}
int main(void)
{
// puntatore a una funzione di tipo T
int(*ptr_to_f)(int, int) = sum;
// puntatore a una funzione di tipo W
double(*ptr_to_f_2)(void) = foo;
// puntatore a una funzione di tipo T <---> puntatore a una funzione di tipo W
// DEFINITO
ptr_to_f = ptr_to_f_2;
// ptr_to_f riconvertito nuovamente in ptr_to_f_2
// COMPORTAMENTO 'RIPRISTINATO' ptr_to_f_2 è di tipo (void) -> double
// e referenzia correttamente il codice da invocare
ptr_to_f_2 = ptr_to_f;
// puntatore a funzione di tipo (float, float) -> float che contiene
// un riferimento a una funzione di tipo (int, int) -> int
// DEFINITO
float (*ptr_to_f_3)(float, float) = sum;
// invocazione di sum per il tramite di ptr_to_f_3
// il tipo della funzione riferita è diverso dal tipo della funzione di cui
// l'identificatore ptr_to_f_
// COMPORTAMENTO NON DEFINITO
(*ptr_to_f_3)(5, 6);
...
}