Nei titoli e nei testi troverete qualche rimando cinematografico (ebbene si, sono un cinefilo). Se non vi interessano fate finta di non vederli, già che non sono fondamentali per la comprensione dei post...

Di questo blog ho mandato avanti, fino a Settembre 2018, anche una versione in Spagnolo. Potete trovarla su El arte de la programación en C. Buona lettura.

sabato 24 novembre 2018

I Misteri delle Funzioni del C
come funzionano alcune funzioni speciali in C - pt.1

Mr. Neville: quattro indumenti e una scala non ci portano a un cadavere.
Sig.ra Talmann: Sig. Neville, non ho parlato di un cadavere.
Nel gran film I misteri del giardino di Compton House, un disegnatore (un programmatore?) riceve l'incarico di disegnare 12 disegni (12 funzioni?) di una aristocratica casa di campagna: si scoprirà, poi, che ogni disegno contiene e rappresenta un mistero (come funziona questa funzione?). Ebbene, girando su vari siti di programmazione (tipo l'ottimo stackoverflow, per intenderci), ho notato che, a volte, anche programmatori "scafati" hanno dubbi su concetti di base (...ammettiamolo, siamo umani...). Ecco, oggi parleremo di funzioni: sono il pane quotidiano di ogni programmatore C eppure, in alcuni casi, hanno un funzionamento misterioso. Vediamone qualcuna...
...siamo qui riuniti per spiegare una funzione misteriosa...
Cominciamo con un caso semplice semplice, che possiamo chiamare così:

1) Il mistero delle funzioni con un parametro array
Quando dobbiamo passare un array a una funzione è meglio usare come argomento un array? E quando dobbiamo passare un pointer è meglio usare un argomento pointer? Beh, visto che il caso è semplice diamo subito la risposta: È la stessa cosa! Gli argomenti array di una funzione vengono (magicamente) trasformati in pointer dal compilatore: è un comportamento standard  ben documentato anche sul mitico K&R:
"Quando il nome di un vettore viene passato ad una funzione, ciò che viene passato è la posizione dell’elemento iniziale. All’interno della funzione chiamata, questo argomento è una variabile locale, quindi un nome di vettore passato come parametro è in realtà un puntatore, ovvero una variabile che contiene un indirizzo." (Il Linguaggio C - 2a.ed. - B.W.Kernighan, D.M.Ritchie)
Comunque, già che ci siamo, vediamo due piccoli esempi che ci serviranno anche più avanti. Vai con l'esempio che usa l'array!
// prototipi locali
void myfunc(char arg[]);

// funzione main
int main(int argc, char *argv[])
{
    // set variabile e chiamo myfunc()
    char string[] = "pippo";
    myfunc(string);

    return 0;
}

// funzione myfunc con array
// anche mettendo la dimensione (i.e.: char arg[100]) il codice 
// generato è identico
void myfunc(char arg[])
{
    char a = arg[3]
}
E, a seguire, l'esempio che usa il pointer!
// prototipi locali
void myfunc(char *arg);

// funzione main
int main(int argc, char *argv[])
{
    // set variabile e chiamo myfunc()
    char *string = "pippo";
    myfunc(string);

    return 0;
}

// funzione myfunc con pointer
// questa è la versione a cui viene ricondotta dal compilatore una eventuale 
// versione con array
void myfunc(char *arg)
{
    char a = arg[3];
}
Allora: le due funzioni myFunc() sono perfettamente equivalenti, o meglio: la myFunc() con l'array si trasforma in quella con il pointer (e non importa se scriviamo anche la dimensione dell'array: 10, 100, 1000, non cambia nulla!). E per quelli che sono come San Tommaso (che non ci crede se non ci mette il naso) consiglio un piccolo esperimento: compilare per ottenere l'assembly (con gcc -S, per esempio) e verificare che il codice delle funzioni è identico, e che le uniche differenze sono nel corpo del main()... ma non dovrebbe essere praticamente identico anche quest'ultimo? Forse che "char *string = "pippo";" sviluppa codice assembly molto diverso da quello di "char string[] = "pippo";"? Ebbene si, quindi siamo pronti per analizzare un altro mistero:

2) Il mistero delle funzioni che restituiscono una stringa letterale
Sicuramente saprete tutti che una funzione non deve mai restituire l'indirizzo di una variabile locale (vabbè: una variabile automatica allocata nello stack della funzione), perché quando la funzione "esce" la variabile va out-of-scope e l'indirizzo passato al chiamante non è più valido. E allora come si fa una funzione che ritorna una stringa? Vediamo un esempio:
#include <stdio.h>

// prototipi locali
char *myfunc1(void);
char *myfunc2(void);

// funzione main
int main(int argc, char *argv[])
{
    // mostro il risultato delle funzioni
    printf("myfunc1(): %s\n", myfunc1());
    printf("myfunc2(): %s\n", myfunc2());

    return 0;
}

// funzione myfunc1 - questa funziona
char *myfunc1(void)
{
    char *string = "pippo";
    return string;  // Ok: string è (implicitamente) una variabile statica
}

// funzione myfunc2 - questa non funziona
char *myfunc2(void)
{
    char string[] = "pluto";
    return string;  // NOk: string è una variabile locale (con Warning in compilazione!)
} 
a prima vista anche myFunc1() sembrerebbe un esempio da manuale di come non fare una buona funzione! Eppure funziona (leggere i commenti), e non per caso. Il fatto è che la stringa string in myFunc1() non è una variabile locale, infatti viene (implicitamente) allocata, direttamente dal compilatore, in una zona Read Only della memoria e quindi è, a tutti gli effetti, una variabile statica. Per cui l'esempio qui sopra funziona ed è la maniera migliore di fare una funzione che ritorna una stringa. Ed attenzione: se invece usiamo un array, come è il caso della myFunc2() non funziona: questo si che viene trattato come variabile locale (ecco perché i due main() del mistero numero 1 erano diversi!). Notare, infine, che myFunc1() e la sua variabile string interna dovrebbero essere dichiarate const, visto che la stringa "pippo" ritornata non è modificabile, quindi usando il qualificatore const permettiamo al compilatore di segnalarci eventuali operazioni non permesse (provate a modificare il contenuto di una inmutable string, vedrete che bell'effetto!). Ho scritto "dovrebbero essere dichiarate const" in corsivo perché non è obbligatorio: è solo consigliabile farlo, ma non è che rende il codice a prova di bomba...

E dopo aver visto i due casi precedenti siamo ben preparati e pronti a vedere il terzo mistero:

3) Il mistero delle funzioni che restituiscono un char pointer
Nella libc ci sono un bel po' di funzioni che restituiscono un char* (per gli amici "char star"), come ad esempio quelle delle famiglia string. Come sono fatte? Ci sono alternative valide alla struttura "classica"? Vediamo un esempio con una implementazione minimale delle strcpy() e strncpy(), senza controlli, senza ottimizzazioni, senza tutto, ma per un esempio è più che sufficiente. Vediamo il codice, che include anche un piccolo e semplice main() di test:
#include <stdio.h>
#include <stdlib.h>
#include <string.h>

// prototipi locali
char *myStrcpy(char *dest, const char *src);
char *myStrncpy(char *dest, const char *src, size_t len);
char *myStrncpyMalloc(char *dest, const char *src, size_t len);

// funzione main
int main(int argc, char *argv[])
{
    // eseguo myStrcpy() e mostro il risultato
    char *src1 = "pippo";
    char dest1[32];
    printf("myStrcpy(): dest = %s\n", myStrcpy(dest1, src1));

    // eseguo myStrncpy() e mostro il risultato
    char *src2 = "paperino";
    char dest2[32];
    printf("myStrncpy(): dest = %s\n", myStrncpy(dest2, src2, strlen(src2)));

    // eseguo myStrncpyMalloc() e mostro il risultato
    char *src3 = "paperone";
    char *dest3 = myStrncpyMalloc(dest3, src3, strlen(src3));
    printf("myStrncpyMalloc(): dest = %s\n", dest3);
    free(dest3);    // ricordarsi di questa free()

    return 0;
}

// myStrcpy - versione semplificata della strcpy() della libc
char *myStrcpy(char *dest, const char *src)
{
    // salvo localmente dest
    char *my_dest = dest;

    // loop per copiare src sulla copia locale di dest
    while (*src)
       *my_dest++ = *src++;

    // ritorno il dest salvato
    return dest;
}

// myStrncpy - versione semplificata della strncpy() della libc
char *myStrncpy(char *dest, const char *src, size_t len)
{
    // salvo localmente dest
    char *my_dest = dest;

    // loop per copiare src sulla copia locale di dest (copio solo <len> caratteri)
    while (*src && len--)
       *my_dest++ = *src++;

    // aggiungo il terminatore di stringa
    *my_dest = 0;

    // ritorno il dest salvato
    return dest;
}

// myStrncpy - versione con malloc interna di myStrncpy()
char *myStrncpyMalloc(char *dest, const char *src, size_t len)
{
    // alloco dest (N.B.: la free() la deve fare il chiamante della myMemcpyMalloc())
    dest = malloc(len + 1); // il +1 è per il terminatore di stringa

    // salvo localmente dest
    char *my_dest = dest;

    // loop per copiare src sulla copia locale di dest (copio solo <len> caratteri)
    while (*src && len--)
       *my_dest++ = *src++;

    // aggiungo il terminatore di stringa
    *my_dest = 0;

    // ritorno il dest salvato
    return dest;
}
Avete notato la semplicità del meccanismo? Nella myStrcpy() Si salva localmente il destino <dest> e, in un loop, si copia "byte-a-byte" <src> sulla copia locale di <dest> (fermandosi quando si raggiunge il terminatore di stringa di <src>). Infine, si restituisce il pointer originale <dest> e non la sua copia. E perché l'originale? perchè l'operazione viene eseguita incrementando i pointer di <src> e <dest>, quindi dobbiamo restituire il pointer originale e non quello incrementato nel loop. E la myStrncpy()? Come si nota è formalmente identica alla myStrcpy(), con la unica differenza che usa un argomento addizionale <len> per copiare solo <len> caratteri (invece di arrivare fino al terminatore di stringa). Il terminatore viene aggiunto poi internamente alla funzione, proprio perché è possibile che la copia non arrivi fino alla fine della stringa originale.

E visto che volevo strafare (si, strafacciamo: Punto! Due punti!... ma sì, fai vedere che abbondiamo... Abbondandis in abbondandum...) ho creato e aggiunto anche un altra funzione, la myStrncpyMalloc(), che è una versione poco canonica della strncpy() che si occupa anche di allocare internamente (con malloc()) il destino della copia: è molto comoda, visto che possiamo passare direttamente un pointer non allocato nell'argomento <dest> (vedi il main() qui sopra), ma poi dobbiamo ricordarci di liberarlo! (vedi di nuovo il main() qua sopra). E se ci dimentichiamo di fare la free() sono dolori...

Beh, direi che con i tre misteri di oggi abbiamo già svelato molti dettagli arcani di alcune funzioni C. Ne rimangono ancora molti, e avremo tempo di parlarne più avanti nella seconda parte del post dove, sicuramente, troveremo un altro vecchio amico: void* (per gli amici "void star")... e mi raccomando: non trattenete il respiro nell'attesa...

Ciao e al prossimo post!

Nessun commento:

Posta un commento