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 ():

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:

Definizione

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:

  1. 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.
  2. 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:

  1. x è un array.
  2. di puntatori.
  3. a funzioni senza argomenti.
  4. che ritornano puntatore a int.
Esempio di Dichiarazione Complessa
Figura 1: Esempio di Dichiarazione Complessa

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 ***/
Nota

Attenzione Le funzioni non possono restituire array.

Le funzioni non possono restituire funzioni:

int g(int)(int);  /*** ERRATO ***/
Nota

Attenzione Le funzioni non possono restituire funzioni.

Non sono possibili neppure array di funzioni:

int a[10](int);  /*** ERRATO ***/
Nota

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.