[successivo] [precedente] [inizio] [fine] [indice generale] [indice ridotto] [indice analitico] [home] [volume] [parte]


Capitolo 578.   C: le funzioni

Per comprendere come «funzionano» le funzioni nel linguaggio C, occorre fare mente locale all'uso della pila dei dati con il linguaggio macchina. Questo capitolo ha lo scopo di chiarire alcuni concetti, partendo dal ripasso della pila dei dati.

578.1   Pila dei dati

Dal punto di vista del linguaggio macchina, generalmente si dispone di una pila di dati che si sviluppa a partire da un certo indirizzo di memoria, utilizzando di volta in volta indirizzi inferiori della stessa. Attraverso la pila dei dati, prima della chiamata di una funzione, gli argomenti vengono passati alla stessa aggiungendoli alla pila; successivamente, all'interno della funzione, tutte le variabili locali vengono ottenute facendo crescere ulteriormente la pila. Al termine dell'esecuzione della funzione, la pila viene ridotta allo stato precedente alla chiamata, espellendo le variabili locali e i parametri della chiamata.

Figura 578.1. Semplificazione del meccanismo attraverso cui si passano gli argomenti a una funzione e si gestiscono le variabili locali.

pila dei dati

Naturalmente, dal momento che la pila di dati viene gestita attraverso la memoria centrale, la quale consente un accesso diretto ai dati, tramite un indirizzo, nella pila si possono gestire dati di tutti i tipi, volendo anche degli array. A proposito degli array, quando questi sono creati all'interno delle funzioni, pertanto attraverso l'uso della pila dei dati, al compilatore non è necessario sapere preventivamente le dimensioni di questi, perché lo spazio che usano nella memoria è allocato dinamicamente, tramite la pila.

578.2   Dichiarazione e chiamata di una funzione

La dichiarazione di una funzione prevede l'indicazione del tipo di variabili che compongono i parametri, allo scopo di far sapere al compilatore in che modo inserire gli argomenti nella pila, al momento della chiamata. Si osservi l'esempio seguente in cui si dichiara una funzione con due parametri molto semplici: un intero normale e un intero di dimensione «doppia».

void f (int x, long long int y)
{
    ...
    ...
}

Partendo dal presupposto che la pila dei dati sia gestita a blocchi di «parole» del microprocessore, si può ipotizzare ragionevolmente in che modo siano impilati gli argomenti della chiamata. Si suppone di chiamare la funzione nel modo seguente e che la parola sia da 32 bit:

...
f (0x13579BDF, 0x123456789ABCDEF);
...

Alla chiamata della funzione, i parametri dovrebbero apparire nella pila come nella figura successiva, trascurando il problema dell'inversione eventuale dei byte:

pila dei dati

Come si vede, gli argomenti vengono impilati in ordine inverso, in modo tale che il primo argomento appaia all'inizio della pila.

Ci sono molti dettagli da definire sul come vadano impilati gli argomenti di una chiamata; in particolare è da chiarire in che modo vadano trattati i dati la cui dimensione è inferiore alla parola del microprocessore, così come per quelli che si articolano in strutture. Questi dettagli vanno chiariti quando si vogliono scrivere funzioni da usare assieme a codice scritto in linguaggio assemblatore, oppure anche per altri linguaggi, se per quelli si utilizzano compilatori non conformi a quello usato per il C.

578.3   Elenco indefinito di parametri

Il linguaggio C ammette che le funzioni siano dichiarate con almeno un parametro esplicito e un elenco indefinito di parametri successivi. In altre parole, si ammette che ci sia un parametro certo e un elenco, eventuale, di altri parametri sconosciuti. Questo avviene, per esempio, con funzioni standard quali printf():

int printf (const char *formato, ...);

Quando si chiama una funzione del genere, gli argomenti successivi al primo, se riguardano valori numerici, vengono «promossi» in modo tale da avere una dimensione minima di riferimento. Per la precisione, i valori interi di rango inferiore a quello di un intero comune, sono convertiti al livello di intero int (con segno o senza, in base alle caratteristiche di partenza); i valori in virgola mobile, se sono espressi secondo un formato di rango inferiore a double, vengono trasformati semplicemente in double. Gli interi e i valori in virgola mobile di rango superiore, rimangono invariati.

È da osservare che, se si tenta di passare come argomento un valore che occupa uno spazio inferiore alla dimensione della parola del microprocessore, pur dichiarando tutti i parametri è molto probabile che il compilatore debba utilizzare ugualmente una parola intera, riempiendo in qualche modo lo spazio restante con dati nulli; pertanto, in presenza di parametri di dimensione non stabilita, è più che appropriata la promozione predefinita degli argomenti a valori multipli della parola.

Viene mostrato un esempio di programma contenente una funzione con un numero indefinito di parametri, nella quale, gli argomenti della chiamata vengono comunque estratti dalla pila dei dati, conoscendo le dimensioni usate nella chiamata. L'esempio funziona con un compilatore GNU C e serve solo per comprendere il meccanismo, ma per il momento non rappresenta il modo corretto di agire a questo proposito:

:-(

#include <stdio.h>

void f (int w,...)
{
    //
    // Traduce l'indirizzo di «w» nel puntatore «p».
    //
    char *p = (char *) &w;
    //
    // Sposta il puntatore all'inizio del secondo parametro.
    //
    p = p + sizeof w;
    //
    // Mostra il valore del primo e del secondo parametro.
    //
    printf ("w = %d; ", w);
    printf ("x = %Lf; ", *((long double *)p));
    //
    // Sposta il puntatore all'inizio del terzo parametro.
    //
    p = p + sizeof (long double);
    //
    // Mostra il terzo parametro.
    //
    printf ("y = %lld; ", *((long long int *)p));
    //
    // Sposta il puntatore all'inizio del quarto parametro.
    //
    p = p + sizeof (long long int);
    //
    // Mostra il quarto parametro.
    //
    printf ("z = %d\n", *((int *)p));
    //
    return;
}

int main (int argc, char *argv[])
{
    f (10, (long double)12.34, (long long int)13, 14);
    return 0;
}

Come si vede, per raggiungere gli argomenti successivi al primo, conoscendo le loro caratteristiche, si scandisce in pratica la memoria occupata dalla pila dei dati, prendendo come riferimento l'indirizzo del primo parametro, il quale costituisce il riferimento certo. Si misura la dimensione del primo parametro e si aggiusta il puntatore in modo da posizionarsi dopo la fine di questo, sapendo che da lì in poi si trovano gli argomenti successivi. Il puntatore è di tipo char *, in modo da poterlo gestire a unità di «caratteri», conformemente al valore prodotto dall'operatore sizeof. Se tutto funziona come previsto, il programma mostra correttamente il messaggio seguente:

w = 10; x = 12.340000; y = 13; z = 14

Il modo corretto di estrapolare i valori dei parametri non dichiarati richiede l'uso di alcune macro-istruzioni della libreria standard stdarg.h. Si osservi come va trasformato l'esempio già apparso per rispettare la formalità standard:

:-)

#include <stdio.h>
#include <stdarg.h>

void f (int w,...)
{
    //
    // Dichiara le variabili che servono a contenere
    // gli argomenti privi di parametri formali.
    //
    long double x;
    long long int y;
    int z;
    //
    // Dichiara il puntatore ai parametri.
    //
    va_list ap;
    //
    // Posiziona il puntatore dopo il primo parametro,
    // ovvero dopo l'ultimo parametro dichiarato esplicitamente.
    //
    va_start (ap, w);
    //
    // Estrapola il secondo argomento della chiamata (portando avanti
    // il puntatore di conseguenza.
    //
    x = va_arg (ap, long double);
    //
    // Mostra il valore del primo e del secondo argomento
    // ottenuto dalla chiamata della funzione.
    //
    printf ("w = %d; ", w);
    printf ("x = %Lf; ", x);
    //
    // Estrapola il terzo argomento.
    //
    y = va_arg (ap, long long int);
    //
    // Mostra il terzo argomento.
    //
    printf ("y = %lld; ", y);
    //
    // Estrapola il quarto argomento.
    //
    z = va_arg (ap, int);
    //
    // Mostra il quarto e ultimo argomento.
    //
    printf ("z = %d\n", z);
    //
    // Conclude la scansione degli argomenti.
    //
    va_end (ap);
    //
    return;
}

int main (int argc, char *argv[])
{
    f (10, (long double)12.34, (long long int)13, 14);
    return 0;
}

Come si vede, è necessario incorporare la libreria stdarg.h. All'inizio della funzione si dichiara una variabile di tipo va_list per scandire l'elenco di parametri: si tratta evidentemente di un puntatore (molto probabilmente al tipo char). Subito dopo si inizializza la variabile da usare per la scansione con la macro-istruzione va_start che ha l'apparenza di una funzione. A va_start viene passata la variabile da usare come puntatore per gli argomenti e l'ultimo parametro dichiarato espressamente nella funzione, allo scopo di aggiornare il puntatore e di portarlo all'inizio del primo argomento privo di un parametro esplicito. Successivamente si utilizza la macro-istruzione va_arg, anche questa con l'apparenza di una funzione, per estrapolare l'argomento a cui punta la variabile di tipo va_list, usata per lo scopo, aggiornando conseguentemente la variabile-puntatore, in modo da essere pronta per l'argomento successivo. Al termine si usa va_end, la quale può essere indifferentemente una macro-istruzione o una funzione vera e propria, allo scopo di concludere l'uso del puntatore dichiarato per la scansione dei parametri.

Le macro-istruzioni va_start e va_arg non potrebbero essere realizzate in forma di funzioni. Infatti, va_start utilizza apparentemente come argomento l'ultimo parametro della funzione, ma per calcolare la posizione del parametro successivo servirebbe invece l'indirizzo di tale variabile. In modo analogo, la macro-istruzione va_arg richiede l'indicazione del tipo di dati da estrarre, mentre una funzione vera potrebbe accettare solo la dimensione restituita dall'operatore sizeof; inoltre restituisce un valore dello stesso tipo, mentre una funzione vera può restituire un solo tipo prestabilito.

Nell'esempio non si vede cosa accade quando si trasmette un argomento costituito da un carattere (char). In tal caso bisogna tenere in considerazione l'effetto della promozione a intero; pertanto, la macro-istruzione va_arg va usata indicato un tipo int (e non un tipo char). Lo stesso dicasi per i valori in virgola mobile, che vanno estratti prevedendo un formato double, anche se nell'argomento originale dovesse trattarsi di float (e ammesso che l'argomento non sia espresso in un formato ancora più grande).

578.4   Annotazioni su «printf()» e altre funzioni simili

Da quanto descritto a proposito della promozione dei valori numerici, interi o in virgola mobile, si comprende che le rappresentazioni di valori numerici vanno fatte preferibilmente a partire da interi di tipo int o da valori in virgola mobile di tipo double. Si osservino gli esempi seguenti:

Nel caso della funzione scanf(), questi problemi non ci sono, perché gli argomenti variabili sono costituiti tutti da puntatori ad aree di memoria che devono essere in grado di contenere le informazioni da inserire.

578.5   Costante predefinita «__func__»

Lo standard del linguaggio prescrive che, se all'interno di una funzione viene usato il nome __func__, questo si deve tradurre nel nome della funzione che lo contiene. In pratica, il compilatore che incontra questo nome, dichiara automaticamente, all'interno della funzione, la costante seguente:

static const char __func__[] = "nome_funzione";

L'esempio seguente mostra in che modo se ne potrebbe fare uso:

#include <stdio.h>
void f (void)
{
    printf ("Sono nella funzione \"%s\".\n", __func__);
}
int main (int argc, char *argv[])
{
    f ();
    return 0;
}

Una volta compilato il programma, eseguendolo si ottiene:

Sono nella funzione "f".

Appunti di informatica libera 2008 --- Copyright © 2000-2008 Daniele Giacomini -- <appunti2 (ad) gmail·com>


Dovrebbe essere possibile fare riferimento a questa pagina anche con il nome c_le_funzioni.htm

[successivo] [precedente] [inizio] [fine] [indice generale] [indice ridotto] [indice analitico] [home]

Valid ISO-HTML!

CSS validator!

Gjlg Metamotore e Web Directory