Barninga Z
a- a+

Puntatori a funzioni

Puntatori a funzioni

Credevate di esservene liberati? Ebbene no! Rieccoci a parlare di puntatori... Sin qui li abbiamo presentati come variabili un po' particolari, che contengono l'indirizzo di un dato piuttosto che un dato vero e proprio. E' giunto il momento di rivedere tale concetto, di ampliarlo, in quanto possono essere dichiarati puntatori destinati a contenere l'indirizzo di una funzione.

Un puntatore a funzione è dunque un puntatore che non contiene l'indirizzo di un intero, o di un carattere, o di un qualsiasi altro tipo di dato, bensì l'indirizzo del primo byte del codice di una funzione. Vediamone la dichiarazione:

int (*funcPtr)(char *string);

 

Nell'esempio funcPtr è un puntatore ad una funzione che restituisce un int e accetta quale parametro un puntatore a char. La sintassi può apparire complessa, ma un esame più approfondito rivela la sostanziale analogia con i puntatori che già conosciamo. Innanzitutto, l'asterisco che precede il nome funcPtr ne rivela inequivocabilmente la natura di puntatore. Anche la parola chiave int ha un ruolo noto: indica che l'indirezione del puntatore restituisce un intero. Trattandosi di un puntatore a funzione, funcPtr è seguito dalle parentesi tonde contenenti la lista dei parametri della funzione. Sono proprio queste parentesi a indicare che funcPtr è puntatore a funzione. Restano da spiegare le parentesi che racchiudono *funcPtr: esse sono indispensabili per distinguere la dichiarazione di un puntatore a funzione da un prototipo di funzione. Se riscriviamo la dichiarazione dell'esempio omettendo la prima coppia di parentesi, otteniamo

int *funcPtr(char *string);

 

cioè il prototipo di una funzione che restituisce un puntatore ad intero e prende come parametro un puntatore a carattere.

Poco fa si è detto che l'indirezione di funcPtr restituisce un intero. Che significato ha l'indirezione di un puntatore a funzione? Quando si ha a che fare con puntatori a "dati" , il concetto è piuttosto semplice: l'indirezione rappresenta il dato che si trova all'indirizzo contenuto nel puntatore stesso. Ma all'indirizzo contenuto in un puntatore a funzione si trova una parte del programma, cioè vero e proprio codice eseguibile: allora ha senso parlare di indirezione di un puntatore a funzione solo con riferimento al dato restituito dalla funzione che esso indirizza. Ma perché una funzione possa restituire qualcosa deve essere eseguita: e proprio qui sta il bello, dal momento che l'indirezione di un puntatore a funzione rappresenta una chiamata alla funzione indirizzata. Vediamo funcPtr all'opera:

#include 

...
    int iVar;
    char *cBuffer;
    ....
    funcPtr = strlen;
    ....
    iVar = (*funcPtr)(cBuffer);
    ....

 

Nell'esempio, a funcPtr è assegnato l'indirizzo della funzione di libreria strlen(), il cui prototipo si trova in STRING.H, che accetta quale parametro un puntatore a stringa e ne restituisce la lunghezza (sotto forma di intero). Se ne traggono alcune interessanti indicazioni: per assegnare ad un puntatore a funzione l'indirizzo di una funzione basta assegnargli il nome di quest'ultima. Si noti che il simbolo strlen non è seguito dalle parentesi, poiché in questo caso non intendiamo chiamare strlen() e assegnare a funcPtr il valore che essa restituisce, bensì assegnare a funcPtr l'indirizzo a cui strlen() si trova[9]. Inoltre, il tipo di dato restituito dalla funzione e la lista dei parametri devono corrispondere a quelli dichiarati col puntatore: tale condizione, in questo caso, è soddisfatta.

Infine, nell'esempio compare anche la famigerata indirezione del puntatore: come si vede, al parametro formale della dichiarazione è stato sostituito il parametro attuale (come in qualsiasi chiamata a funzione) e al posto dell'indicatore del tipo restituito troviamo, da destra a sinistra, l'operatore di assegnamento e la variabile che memorizza quel valore.

Va sottolineato che l'indirezione è perfettamente equivalente alla chiamata alla funzione indirizzata dal puntatore: in questo caso a

    iVar = strlen(cBuffer);

 

Allora perché complicarsi la vita con i puntatori? I motivi sono molteplici. A volte è indispensabile conoscere gli indirizzi di alcune routine per poterle gestire correttamente[10]. In altri casi l'utilizzo di puntatori a funzione consente di scrivere codice più efficiente: si consdieri l'esempio che segue.

    if(a > b)
        for(i = 0; i < 1000; i++)
            funz_A(i);
    else
        for(i = 0; i < 1000; i++)
            funz_B(i);

 

Il frammento di codice può essere razionalizzato mediante l'uso di un puntatore a funzione, evitando di scrivere due cicli for quasi identici:

    void (*fptr)(int i);
    ....
    if(a > b)
        fptr = funz_A;
    else
        fptr = funz_B;
    for(i = 0; i < 1000; i++)
        (*fptr)(i);

 

Più in generale, l'uso dei puntatori a funzione si rivela di grande utilità quando, nello sviluppare l'algoritmo, non si può determinare a priori quale funzione deve essere chiamata in una certa situazione, ma è possibile farlo solo al momento dell'esecuzione, dall'esame dei dati elaborati. Un esempio può essere costituito dalla cosiddetta programmazione per flussi guidati da tabelle, nella quale i dati in input consentono di individuare un elemento di una tabella contenente i puntatori alle funzioni richiamabili in quel contesto.

Per studiare nel concreto una applicazione del concetto appena espresso si può pensare ad una programma in grado di visualizzare un sorgente C eliminando tutti i commenti introdotti dalla doppia barra "//". In pratica si tratta di passare alla riga di codice successiva quando si incontra tale sequenza di caratteri: analizzando il testo carattere per carattere, bisogna visualizzare tutti i caratteri letti fino a che si incontra una barra. In questo caso, per decidere che cosa fare, occorre esaminare il carattere successivo: se è anch'esso una barra si passa alla riga successiva e si riprendono a visualizzare i caratteri; se non lo è, invece, deve essere visualizzato, ma preceduto da una barra, e l'elaborazione prosegue visualizzando i caratteri incontrati.

I possibili stati del flusso elaborativo, dunque, sono due: elaborazione normale, che prevede la visualizzazione del carattere, e attesa, indotto dall'individuazione di una barra. La situazione complessiva delle azioni da intraprendere può essere riassunta in una tabella, ogni casella della quale rappresenta le azioni da intraprendere quando si verifichi una data combinazione tra stato elaborativo attuale e carattere incontrato.

 

AZIONI DA INTRAPRENDERE
Carattere incontrato
Stato elaborativo
Barra "/" Altro carattere
Elaborazione normale Non visualizza il carattere
Legge il carattere successivo
Passa in stato "Attesa"
Visualizza il carattere
Legge il carattere successivo
Resta in stato "Normale"
Attesa carattere successivo Non visualizza il carattere
Legge la riga successiva
Passa in stato "Normale"
Visualizza "/" e il carattere
Legge il carattere successivo
Passa in stato "Normale"

Circa il trattamento del carattere, le possibili situazioni sono tre: visualizzazione, non visualizzazione, e visualizzazione del carattere stesso preceduto da una barra. La scansione del file può proseguire in due modi diversi: carattere successivo o riga successiva. Infine, si può avere il passaggio dallo stato normale a quello di attesa, il viceversa, o il permanere nello stato normale. Si tratta di una situazione un po' intricata, ma facilmente trasformabile in algoritmo utilizzando proprio i puntatori a funzione.

Quello che ci occorre è, in primo luogo, un ciclo di controllo del flusso elaborativo: il guscio esterno del programma consiste nella lettura del file riga per riga e nell'analisi della riga letta carattere per carattere.

#include 

#define  MAXLIN    256

void main(void);

void main(void)
{
    char line[MAXLIN], *ptr;

    while(gets(line)) {
        for(ptr = line; *ptr; ) {
            ....
        }
        printf("
");
    }
}

 

Ecco fatto. La funzione di libreria gets() legge una riga dallo standard input[11] e la memorizza nell'array di caratteri il cui indirizzo le è passato quale parametro. Dal momento che essa restituisce NULL se non vi è nulla da leggere, il ciclo while() è iterato sino alla lettura dell'ultima riga del file. Il ciclo for() scandisce la riga carattere per carattere e procede sino a quando è incontrato il NULL che chiude la riga. E' compito del codice all'interno del ciclo incrementare opportunamente ptr. All'uscita dal ciclo for() si va a capo[12].

A questo punto entrano trionfalmente in scena i puntatori a funzione. Per elaborare correttamente una singola riga ci occorrono quattro diverse funzioni, ciascuna in grado di manipolare un dato carattere come descritto in una delle quattro caselle della nostra tabella. Vediamole:

#include 
#include 

#define  NORMAL   0
#define  WAIT     1

char *hideLetterInc(char *ptr)  
// non visualizza il carattere e restituisce
{      // il puntatore incrementato (tabella[0][0])
    extern int nextStatus;

    nextStatus = WAIT;
    return(ptr+1);
}

char *sayLetterInc(char *ptr)   
// visualizza il carattere e restituisce il
{      // puntatore incrementato (tabella[0][1])
    extern int nextStatus;

    nextStatus = NORMAL;
    printf("%c" ,*ptr);
    return(ptr+1);
}

char *hideLetterNextLine(char *ptr)     
// non visualizza il carattere e
{      // restituisce l'indirizzo del NULL
    extern int nextStatus; // terminator (tabella[1][0])

    nextStatus = NORMAL;
    return(ptr+(strlen(ptr));
}

char *sayBarLetterInc(char *ptr)        
// visualizza il carattere preceduto da una
{      // barra e restituisce il puntatore
    extern int nextStatus; // incrementato (tabella[1][1])

    nextStatus = NORMAL;
    printf("/%c" ,*ptr);
    return(ptr+1);
}

 

Come si vede, il codice delle funzioni è estremamente semplice. Tuttavia, ciascuna esaurisce il compito descritto in una singola cella della tabella, compresa la "decisione" circa lo stato ("normale" o "attesa") che vale per il successivo carattere da esaminare: non ci resta che creare una tabella analoga a quella presentata poco fa, ma contenente i puntatori alle funzioni.

 

FUNZIONI DA CHIAMARE
Carattere incontrato
Stato elaborativo
Barra "/" Altro carattere
Elaborazione normale hideLetterInc() sayLetterInc()
Attesa carattere successivo hideLetterNextLine() sayBarLetterInc()

Ed ecco la codifica C della tabella di puntatori a funzione:

char *hideLetterInc(char *ptr);
char *sayLetterInc(char *ptr);
char *hideLetterNextLine(char *ptr);
char *sayBarLetterInc(char *ptr);

char *(*funcs[2][2])(char *ptr) = {
    {hideLetterInc,      sayLetterInc},
    {hideLetterNextLine, sayBarLetterInc}
};

 

Lo stato di elaborazione è, per default, "Normale" e viene individuato dalle funzioni ad ogni carattere trattato, il quale è la seconda coordinata necessaria per individuare il puntatore a funzione opportuno all'interno della tabella. Ora siamo finalmente in grado di presentare il listato completo del programma.

#include       // per printf() e gets()
#include      // per strlen()

#define  MAXLIN    256
#define  NORMAL      0
#define  WAIT        1
#define  BAR         0
#define  NON_BAR     1

void main(void);
char *hideLetterInc(char *ptr);
char *sayLetterInc(char *ptr);
char *hideLetterNextLine(char *ptr);
char *sayBarLetterInc(char *ptr);

extern int nextStatus = NORMAL;

void main(void)
{
    static char *(*funcs[2][2])(char *ptr) = {    
// e' static perche' e'
        {hideLetterInc, sayLetterInc},       
/ dichiarato ed inizializzato
        {hideLetterNextLine, sayBarLetterInc}        
// in una funzione
    };
    char line[MAXLIN], *ptr;
    int letterType;

    while(gets(line)) {
        for(ptr = line; *ptr; ) {
            switch(*ptr) {
                case '/':
                    letterType = BAR;
                    break;
                default:
                    letterType = NON_BAR;
            }
            ptr = (*funcs[nextStatus][letterType])(ptr);
        }
        printf("
");
    }
}

char *hideLetterInc(char *ptr)  
// non visualizza il carattere e restituisce
{      // il puntatore incrementato (tabella[0][0])
    extern int nextStatus;

    nextStatus = WAIT;
    return(ptr+1);
}

char *sayLetterInc(char *ptr)   
// visualizza il carattere e restituisce il
{      // puntatore incrementato (tabella[0][1])
    extern int nextStatus;

    nextStatus = NORMAL;
    printf("%c" ,*ptr);
    return(ptr+1);
}

char *hideLetterNextLine(char *ptr)     
// non visualizza il carattere e
{      // restituisce l'indirizzo del NULL
    extern int nextStatus; // terminator (tabella[1][0])

    nextStatus = NORMAL;
    return(ptr+(strlen(ptr)));
}

char *sayBarLetterInc(char *ptr)        
// visualizza il carattere preceduto da una
{      // barra e restituisce il puntatore
    extern int nextStatus; // incrementato (tabella[1][1])

    nextStatus = NORMAL;
    printf("/%c" ,*ptr);
    return(ptr+1);
}

 

Il contenuto del ciclo for() è sorprendentemente semplice[13]. Ma il cuore di tutto il programma è la riga

    ptr = (*funcs[nextStatus][letterType])(ptr);

 

in cui possiamo ammirare il risultato di tutti i nostri sforzi elucubrativi: una sola chiamata a funzione, realizzata attraverso un puntatore, a sua volta individuato nella tabella tramite le "coordinate" nextStatus e letterType, evita una serie di if nidificate e, di conseguenza, una codifica dell'algoritmo sicuramente meno essenziale ed efficiente.

L'esempio evidenzia inoltre quale sia la sintassi della dichiarazione e dell'utilizzo di un array di puntatori a funzione.

Forse può apparire non del tutto chiaro come sia forzata la lettura della riga successiva quando è individuato un commento: il test del ciclo for() determina l'uscita dal medesimo quando l'indirezione di ptr è un byte nullo, e questa è proprio la situazione indotta dalla funzione hideLetterNextLine(), che restituisce un puntatore al null terminator della stringa contenuta in line.

Va ancora sottolineato che nextStatus è dichiarata come variabile globale per... pigrizia: dichiararla all'interno di main() avrebbe reso necessario passarne l'indirizzo alle funzioni richiamate mediante il puntatore, perché queste possano modificarne il valore. Nulla di difficile, ma non era il caso di complicare troppo l'esempio.

Infine, è meglio non montarsi la testa: quello presentato è un programma tutt'altro che privo di limiti. Infatti non è in grado di riconoscere una coppia di barre inserita all'interno di una stringa, e la considera erroneamente l'inizio di un commento; inoltre visualizza comunque tutti gli spazi compresi tra l'ultimo carattere valido di una riga e l'inizio del commento. L'ingrato compito di modificare il sorgente tenendo conto di queste ulteriori finezze è lasciato, come nei migliori testi, alla buona volontà del lettore[14].

Tanto per complicare un po' le cose, anche i puntatori a funzione possono essere near o far. Per chiarire che cosa ciò significhi, occorre ancora una volta addentrarsi in alcuni dettagli tecnici. I processori Intel seguono il flusso elaborativo, istruzione per istruzione, mediante due registri, detti CS e IP (Code Segment e Instruction Pointer): i due nomi ne svelano di per sé le rispettive funzioni. Il primo fissa un'origine ad un certo indirizzo, mentre il secondo esprime l'offset, a partire da quell'indirizzo, della prossima istruzione da eseguire. Se il primo byte di una funzione dista dall'origine meno di 65535 byte è sufficiente, per indirizzarla, un puntatore near, cioè a 16 bit, associato ad IP. In programmi molto grandi è normale che una funzione si trovi in un segmento di memoria diverso da quello corrente[15]: il suo indirizzo deve perciò essere espresso con un valore a 32 bit (un puntatore far, la cui word più significativa è associata a CS e quella meno significativa ad IP).

Bisogna sottolineare che le funzioni stesse possono essere dichiarate near o far. Naturalmente, dichiarare far una funzione non significa forzare il compilatore a creare un programma enorme per poterla posizionare "lontano": esso genera semplicemente un differente algoritmo di chiamata. Tutte le funzioni far sono chiamate salvando sullo stack sia CS che IP (l'indirizzo di rientro dalla funzione), indipendentemente dal fatto che il contenuto di CS debba essere effettivamente modificato. Nelle chiamate di tipo near, invece, viene salvato (e modificato) solo IP. In uscita dalla funzione i valori di CS ed IP sono estratti dallo stack e ripristinati, così da poter riprendere l'esecuzione dall'istruzione successiva alla chiamata a funzione. E' evidente che una chiamata di tipo far può eseguire qualunque funzione, ovunque essa si trovi, mentre una chiamata near può eseguire solo quelle che si trovano effettivamente all'interno del segmento definito da CS. Spesso si dichiara far una funzione proprio per renderla indipendente dalle dimensioni del programma, o meglio dal modello di memoria scelto per compilare il programma. L'argomento è sviluppato con particolare riferimento alle chiamate intersegmento; per ora è sufficiente precisare che proprio dal modello di memoria dipende il tipo di chiamata che il compilatore genera per una funzione non dichiarata near o far in modo esplicito. In altre parole, una definizione come

int funzione(char *buf)
{
    ....
}

 

origina una funzione near o far a seconda del modello di memoria scelto. Analoghe considerazioni valgono per i puntatori: è ancora una volta il modello di memoria a stabilire se un puntatore dichiarato come segue

int (*fptr)(char *buf);

 

è near o far. Qualora si intenda dichiarare esplicitamente una funzione o un puntatore far, la sintassi è ovvia:

int far funzione(char *buf)
{
    ....
}

 

per la funzione, e

int (far *fptr)(char *buf);

 

per il puntatore. Dichiarazioni near esplicite sono assolutamente analoghe a quelle appena presentate.

Infine, le funzioni possono essere definite static:

static int funzione(char *buf)
{
    ....
}

 

per renderle accessibili (cioè "richiamabili") solo all'interno del sorgente in cui sono definite. Non è però possibile, nella dichiarazione di un puntatore a funzione, indicare che questa è static: la riga

static int (*fptr)(char *buf);

 

dichiara un puntatore static a funzione. Ciò appare comprensibile se si considera che, riferita ad una funzione, la parola chiave static ne modifica unicamente la visibilità, e non il tipo di dato restituito (vedere anche quanto detto circa puntatori static e variabili static ed external).

 



Ti potrebbe interessare anche

commenta la notizia

C'è 1 commento
Redazione
Ti interessano altri articoli su questo argomento?
Chiedi alla nostra Redazione!