Puntatori void in linguaggio C
In linguaggio C, definire un puntatore significa specificare anche il tipo di dato a cui il puntatore punta.
Esiste, però, un altro tipo di puntatore, chiamato puntatore void
, che può puntare a qualunque area di memoria, indipendentemente dal tipo di dato che vi è memorizzato. In questa lezione vedremo cosa sono i puntatori void
e come usarli in linguaggio C.
Puntatori void
Nelle lezioni precedenti sulle variabili puntatore abbiamo visto che quando dichiariamo un puntatore in linguaggio C, dobbiamo specificare il tipo di dato a cui il puntatore punta. Ad esempio, se dichiariamo un puntatore a intero, il tipo di dato a cui il puntatore punta è int
.
int *p;
Dichiarare il tipo a cui punta un puntatore è fondamentale per il compilatore. Infatti, conoscendo questa informazione il compilatore è in grado di calcolare correttamente l'offset di memoria per accedere al dato puntato e di effettuare il dereferenziamento del puntatore.
Abbiamo detto, infatti, che l'indirizzo di una qualunque variabile in memoria corrisponde all'indirizzo del suo primo byte.
Per cui, se abbiamo un puntatore a double
, il compilatore sa che per accedere al dato puntato deve leggere, a partire dall'indirizzo puntato, 8 byte di memoria. Infatti un double
occupa 8 byte.
double *p;
Esistono casi, però, in cui programmi che accedono direttamente all'hardware o alla memoria del computer devono memorizzare o passare indirizzi di locazioni di memoria senza conoscere quali tipi di dato vi siano effettivamente memorizzati.
Per questo motivo, in linguaggio C esiste un tipo speciale di puntatore, chiamato puntatore void
.
Un puntatore void
, spesso chiamato puntatore a void
, è un puntatore generico che può essere definito con la sintassi che segue:
void *p;
Questo puntatore può puntare a qualunque area di memoria, indipendentemente dal tipo di dato che vi è memorizzato.
Ad esempio, se vogliamo usare un puntatore void
per memorizzare l'indirizzo di una variabile di tipo int
, possiamo fare quanto segue:
int a = 10;
void *p = &a;
In questo caso, il puntatore p
punta all'indirizzo di memoria di a
, ma il compilatore non sa che tipo di dato è memorizzato in quella locazione di memoria.
Per accedere al dato puntato da un puntatore void
, dobbiamo effettuare un casting esplicito del puntatore al tipo di dato corretto.
int b = *((int *)p);
Nell'esempio di sopra abbiamo effettuato un casting del puntatore p
al tipo int *
e poi abbiamo dereferenziato il puntatore per ottenere il valore di a
.
Puntatore void *
In linguaggio C, un puntatore void
è un puntatore generico che può puntare a qualunque area di memoria, indipendentemente dal tipo di dato che vi è memorizzato.
La sintassi per dichiarare un puntatore void
è la seguente:
void *p;
Per accedere al dato puntato da un puntatore void
, dobbiamo effettuare un casting esplicito del puntatore al tipo di dato corretto:
tipo b = *((tipo *)p);
Quando dereferenziamo un puntatore void
, però, dobbiamo prestare la massima cura.
Cosa accade, infatti, quando effettuiamo un cast esplicito ad un tipo di dato sbagliato?
int a = 10;
void *p = &a;
/* Cosa accade in questo caso? */
double b = *((double *)p);
In questo caso, stiamo effettuando un casting del puntatore p
al tipo double *
, ma il dato puntato da p
è di tipo int
. Mentre un double
occupa 8 byte, un int
ne potrebbe occupare solo 4. Per cui, con l'operazione di dereferenziamento, potremmo accedere ai 4 byte successivi.
In tal caso, il comportamento del programma diventa indefinito. Il programma potrebbe funzionare correttamente, potrebbe generare un errore in fase di esecuzione o potrebbe restituire un valore errato.
Attenzione al casting di puntatori void
Quando dereferenziamo un puntatore void
, dobbiamo fare attenzione a effettuare un casting esplicito al tipo di dato corretto. Se effettuiamo un casting ad un tipo di dato sbagliato, il comportamento del programma diventa indefinito.
Operazioni possibili con puntatori void
I puntatori void
sono a tutti gli effetti dei puntatori. Pertanto è possibile effettuare le stesse operazioni dei puntatori normali su di essi ad eccezione di alcune operazioni.
Vediamo quali sono le operazioni consentite:
-
Assegnamento di un indirizzo:
Ad un puntatore
void
possiamo assegnare l'indirizzo di memoria di una variabile di qualsiasi tipo.int a = 10; void *p = &a;
-
Dereferenziamento:
Possiamo dereferenziare un puntatore
void
a patto che effettuiamo un casting esplicito del puntatore al tipo di dato corretto.int b = *((int *)p);
-
Assegnamento di un puntatore a un altro puntatore:
Possiamo assegnare un puntatore
void
ad un altro puntatorevoid
.void *q = p;
-
Confronto tra puntatori:
Possiamo confrontare due puntatori
void
tra loro.if (p == q) { // ... } else { // ... }
In questo si comportano come i puntatori normali.
Tuttavia, poiché il compilatore non sa il tipo di dato a cui punta un puntatore void
, non possiamo effettuare alcune operazioni che richiedono questa informazione. Le operazioni di aritmetica dei puntatori, infatti, non sono consentite su puntatori void
:
-
La somma o sottrazione di un intero non è consentita:
Non possiamo effettuare la somma o la sottrazione di un intero ad un puntatore
void
./* ERRORE: La somma di un intero ad un * puntatore void non è consentita */ p = p + 1;
/* ERRORE: La sottrazione di un intero ad un * puntatore void non è consentita */ p = p - 1;
-
L'incremento o decremento di un puntatore non è consentito:
Non possiamo incrementare o decrementare un puntatore
void
./* ERRORE: L'incremento di un puntatore void * non è consentito */ p++;
/* ERRORE: Il decremento di un puntatore void * non è consentito */ p--;
-
La sottrazione di due puntatori
void
non è consentita:Non possiamo sottrarre due puntatori
void
./* ERRORE: La sottrazione di due puntatori void * non è consentita */ int diff = p - q;
Ricapitolando:
Operazioni consentite con puntatori void
Sui puntatori void
sono consentite tutte le operazioni consentite sui puntatori normali, ad eccezione delle operazioni di aritmetica dei puntatori.
Le operazioni consentite sui puntatori void
sono:
- Assegnamento di un indirizzo
- Dereferenziamento
- Assegnamento di un puntatore a un altro puntatore
- Confronto tra puntatori
Sebbene esistano queste regole, alcuni compilatori, come GCC, permettono di effettuare alcune operazioni di aritmetica dei puntatori su puntatori void
. Infatti, considerano i puntatori void
come puntatori ad un byte. Pertanto, quando ad esempio si incrementa un puntatore void
, si incrementa di un byte l'indirizzo puntato.
void *p = (void *)0x1000;
p++;
In questo caso, il puntatore p
punta all'indirizzo 0x1001
se usiamo un compilatore che permette questa operazione.
Il nostro consiglio, però, è di evitare di effettuare operazioni di aritmetica dei puntatori su puntatori void
per evitare comportamenti non definiti.
Inoltre, GCC può essere configurato per emettere un warning o un errore quando si effettuano operazioni di aritmetica dei puntatori su puntatori void
. Basta usare l'opzione -Wpointer-arith
per abilitare il warning o -Werror=pointer-arith
per trasformare il warning in errore.
gcc -Wpointer-arith -o programma programma.c
gcc -Werror=pointer-arith -o programma programma.c
Quando usare i puntatori void
A questo punto sorge la domanda: visti i pericoli e le limitazioni nell'uso dei puntatori void
, quando ha senso utilizzarli?
Principalmente esistono tre casi in cui i puntatori void
vengono adoperati.
Il primo caso riguarda l'interfacciamento diretto con l'hardware. Quando si scrivono driver per dispositivi hardware, si lavora direttamente con aree di memoria, soprattutto oggigiorno, dal momento che molti dispositivi sono memory mapped.
Ad esempio, una scheda grafica potrebbe mappare, ossia associare, un'area di memoria alla quale scrivere i pixel direttamente in memoria. un driver o programma potrebbero accedere a questa memoria usando puntatori void
e, successivamente, effettuare il casting esplicito al tipo di dato corretto.
Un secondo utilizzo riguarda invece funzioni che non possono fare assunzioni sul tipo di dato di uno o più parametri.
Un esempio semplice potrebbe essere quello di una funzione che deve effettuare la somma di due valori, ma non sa a priori se i valori sono interi, in virgola mobile o di altro tipo. Allora, la funzione potrebbe accettare due puntatori void
, gli addendi, un puntatore void
per il risultato e un quarto parametro che indica il tipo di dato degli addendi:
void somma(void *a, void *b, void *risultato, int tipo) {
switch (tipo) {
case 0:
/* Somma tra Interi */
*((int *)risultato) = *((int *)a) + *((int *)b);
break;
case 1:
/* Somma tra Double */
*((double *)risultato) = *((double *)a) + *((double *)b);
break;
default:
break;
}
}
Possiamo invocare questa funzione in questo modo:
int a = 10;
int b = 20;
int risultato;
somma(&a, &b, &risultato, 0);
Nelle prossime lezioni vedremo usi più sofisticati di funzioni che accettano puntatori void
.
Un ultimo utilizzo tipico dei puntatori void
riguarda l'allocazione dinamica della memoria. Questo è un argomento fondamentale che vedremo nelle prossime lezioni. Per ora basti sapere che in C è possibile allocare dinamicamente la memoria usando la funzione malloc
e che questa funzione restituisce un puntatore void
. Infatti, questa funzione accetta in ingresso non un tipo ma la dimensione in byte della memoria da allocare.
void *p = malloc(numero_di_byte);
In questo caso, p
è un puntatore void
che punta all'area di memoria allocata dinamicamente. Rimandiamo, però, i dettagli sull'allocazione dinamica della memoria alle prossime lezioni.
Esistono sicuramente altri casi di utilizzo ma questi sono i tre casi tipici. In ogni caso, in codice di alto livello è sempre meglio evitare l'uso dei puntatori void
e bisogna guardare con sospetto codice che fa un uso eccessivo di questi puntatori perché potrebbe rappresentare un errore di progettazione del codice.
In Sintesi
In questa lezione abbiamo visto i puntatori void
in linguaggio C. Abbiamo visto che i puntatori void
sono puntatori generici che possono puntare a qualunque area di memoria, indipendentemente dal tipo di dato che vi è memorizzato.
Abbiamo visto che per accedere al dato puntato da un puntatore void
dobbiamo effettuare un casting esplicito del puntatore al tipo di dato corretto.
Infine, abbiamo visto che i puntatori void
sono a tutti gli effetti dei puntatori, ma che non possiamo effettuare alcune operazioni di aritmetica dei puntatori su di essi.