Dichiaratori in Linguaggio C
Capire i dichiaratori in linguaggio C è essenziale per poter gestire variabili, puntatori e funzioni in modo sicuro ed efficace.
Questo argomento può sembrare complesso a causa delle molteplici combinazioni di simboli (*
, []
, ()
) e delle regole di priorità. Tuttavia, con alcuni principi chiave, è possibile decifrare anche le dichiarazioni più intricate e utilizzare typedef
per semplificare il codice.
Dichiaratori
Un dichiaratore consiste in un identificatore (il nome della variabile o funzione dichiarata), eventualmente preceduto dal simbolo *
o seguito da []
o ()
. Combinando *
, []
e ()
, possiamo creare dichiaratori di complessità sorprendente.
Prima di esaminare i dichiaratori più complicati, ripassiamo i dichiaratori visti nelle lezioni precedenti. Nel caso più semplice, un dichiaratore è soltanto un identificatore, come i
nel seguente esempio:
int i;
I dichiaratori possono anche contenere i simboli *
, []
e ()
:
-
Un dichiaratore che inizia con
*
rappresenta un puntatore:int *p;
-
Un dichiaratore che termina con
[]
rappresenta un array:int a[10];
Le parentesi possono essere lasciate vuote se l'array è un parametro, se ha un inizializzatore o se il suo spazio di archiviazione è
extern
:extern int a[];
Dal momento che
a
è definito altrove nel programma, il compilatore non ha bisogno di conoscerne la lunghezza (nel caso di un array multidimensionale, solo il primo paio di parentesi può essere vuoto).Lo standard C99 fornisce due opzioni aggiuntive per ciò che può comparire tra le parentesi nella dichiarazione di un parametro array:
-
Un dichiaratore che termina con
()
rappresenta una funzione:int abs(int i); void swap(int *a, int *b); int find_largest(int a[], int n);
Il linguaggio C permette di omettere i nomi dei parametri in una dichiarazione di funzione:
int abs(int);
void swap(int *, int *);
int find_largest(int [], int);
Le parentesi possono perfino essere lasciate vuote:
int abs();
void swap();
int find_largest();
Le dichiarazioni nel gruppo qui sopra specificano il tipo di ritorno delle funzioni abs
, swap
e find_largest
, ma non forniscono informazioni sui loro argomenti. Lasciare le parentesi vuote non è lo stesso che scrivere void
tra esse, che indicherebbe l'assenza di argomenti. Lo stile a parentesi vuote delle dichiarazioni di funzione è in gran parte scomparso; è inferiore allo stile di prototipo introdotto in C89, poiché non consente al compilatore di verificare se le chiamate di funzione hanno gli argomenti corretti.
Ricapitolando:
Dichiaratore
Un dichiaratore consiste in un identificatore (il nome della variabile o funzione dichiarata), eventualmente preceduto dal simbolo *
o seguito da []
o ()
.
Se tutti i dichiaratori fossero così semplici, programmare in C sarebbe banale. Sfortunatamente, nella pratica i dichiaratori spesso combinano le notazioni *
, []
e ()
. Ne abbiamo già visti alcuni esempi. Sappiamo, ad esempio, che
int *ap[10];
dichiara un array di 10 puntatori a interi. Sappiamo che
float *fp(float);
dichiara una funzione che ha un parametro di tipo float
e restituisce un puntatore a float
.
Nella lezione sui puntatori a funzione, abbiamo visto che
void (*pf)(int);
dichiara un puntatore a funzione con argomento di tipo int
e valore di ritorno void
.
Per tal motivo, è importante capire come interpretare dichiaratori complessi. Vediamo come fare.
Decodificare Dichiaratori Complessi
Fino a questo punto, non abbiamo avuto troppi problemi a comprendere i dichiaratori. Ma come la mettiamo con dichiaratori come il seguente?
int *(*x[10])(void);
Questo dichiaratore combina *
, []
e ()
, quindi non è ovvio se x
sia un puntatore, un array o una funzione.
Fortunatamente, esistono due regole semplici che ci aiuteranno a capire qualunque dichiarazione, per quanto contorta:
- Leggere sempre i dichiaratori dall'interno verso l'esterno. In altre parole, individuare l'identificatore che si sta dichiarando, quindi iniziare a interpretare la dichiarazione da lì verso l'esterno.
- Quando c'è una scelta, preferire sempre
[]
e()
rispetto a*
. Se*
precede l'identificatore e[]
lo segue, l'identificatore rappresenta un array, non un puntatore. Analogamente, se*
precede l'identificatore e()
lo segue, l'identificatore rappresenta una funzione, non un puntatore. Naturalmente, possiamo sempre usare le parentesi per modificare la priorità normale di[]
e()
rispetto a*
.
Vediamo come queste regole si applicano ai nostri semplici esempi. Nella dichiarazione:
int *ap[10];
l'identificatore è ap
. Poiché *
precede ap
e []
lo seguono, diamo priorità a []
, quindi ap
è un array di puntatori.
Nella dichiarazione:
float *fp(float);
l'identificatore è fp
. Poiché *
precede fp
e ()
lo seguono, diamo priorità a ()
, quindi fp
è una funzione che restituisce un puntatore.
La dichiarazione:
void (*pf)(int);
è un po' più complicata. Dal momento che *pf
è racchiuso tra parentesi, pf
deve essere un puntatore. Ma (*pf)
è seguito da (int)
, quindi pf
deve puntare a una funzione con argomento di tipo int
. La parola void
rappresenta il tipo di ritorno di questa funzione.
Come ultimo esempio, comprendere un dichiaratore complesso spesso comporta un passaggio a zig-zag da un lato all'altro dell'identificatore. Guardiamo questo:
int *(*x[10])(void);
Per prima cosa, individuiamo l'identificatore dichiarato (x
). Vediamo che x
è preceduto da *
e seguito da [10]
. Poiché []
ha priorità su *
, capiamo che x
è un array. Poi ci spostiamo a sinistra per scoprire il tipo degli elementi dell'array (puntatori). Quindi ci spostiamo a destra per vedere che tipo di dati puntano (funzioni senza argomenti). Infine, di nuovo a sinistra per scoprire che ogni funzione restituisce un puntatore a int
. Graficamente, ecco il processo:
x
è un array.- di puntatori.
- a funzioni senza argomenti.
- che ritornano puntatore a
int
.
Alcune Cose Non si Possono Dichiarare
Padroneggiare le dichiarazioni C richiede tempo ed esercizio. L'unica buona notizia è che ci sono alcune cose che non possono essere dichiarate in C. Le funzioni non possono restituire array:
int f(int)[]; /*** ERRATO ***/
Attenzione Le funzioni non possono restituire array.
Le funzioni non possono restituire funzioni:
int g(int)(int); /*** ERRATO ***/
Attenzione Le funzioni non possono restituire funzioni.
Non sono possibili neppure array di funzioni:
int a[10](int); /*** ERRATO ***/
Attenzione Gli array di funzioni non sono ammessi.
In ogni caso, possiamo usare i puntatori per ottenere l'effetto desiderato. Una funzione non può restituire un array, ma può restituire un puntatore a un array. Una funzione non può restituire un'altra funzione, ma può restituire un puntatore a funzione. Gli array di funzioni non sono ammessi, ma un array può contenere puntatori a funzioni.
Usare typedef
per Semplificare le Dichiarazioni
Alcuni programmatori usano i typedef
per aiutare a semplificare le dichiarazioni complesse. Prendiamo in considerazione la dichiarazione di x
che abbiamo esaminato in questa sezione:
int *(*x[10])(void);
Per rendere più chiaro il tipo di x
, potremmo usare la seguente serie di definizioni di tipo:
typedef int *Fcn(void);
typedef Fcn *Fcn_ptr;
typedef Fcn_ptr Fcn_ptr_array[10];
Fcn_ptr_array x;
Se leggiamo queste righe dall'ultima alla prima, vediamo che x
è di tipo Fcn_ptr_array
: un array di valori Fcn_ptr
. Un Fcn_ptr
è un puntatore a tipo Fcn
, e un Fcn
è una funzione che non ha argomenti e restituisce un puntatore a int
.
In Sintesi
In questa lezione abbiamo imparato che:
- Un dichiaratore collega l'identificatore alle informazioni sul tipo (come puntatore, array o funzione).
- Le regole fondamentali per interpretare le dichiarazioni complesse sono: partire dal nome della variabile e dare priorità a
[]
e()
rispetto a*
. - Alcune combinazioni non sono ammesse in C (ad esempio, funzioni che restituiscono array o funzioni), ma esistono soluzioni alternative con l'uso dei puntatori.
- In molti casi, i
typedef
aiutano a rendere il codice più leggibile, consentendo di spezzare una dichiarazione complicata in più definizioni di tipo. - Comprendere chiaramente i dichiaratori evita errori di compilazione e semplifica la manutenzione e l'estensione del codice.