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.

domenica 16 dicembre 2018

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

Sig.ra Talmann: Il tuo discorso, Louis, sta diventando meteorologico.
È giunto il momento di tornare su I misteri del giardino di Compton House, anzi meglio, sulla seconda parte de I Misteri delle Funzioni del C (...e una ripassatina alla prima parte non ci starebbe male, prima di proseguire...). E giuro che cercherò di non cadere in discorsi meteorologici e incomprensibili come Louis Talmann.

...misteri? Quali misteri?...
(...una doverosa premessa: sicuramente per qualcuno, o per molti, i misteri trattati nella prima parte e, probabilmente, anche questi della seconda non sono affatto misteri. Ma voglio ribadire quanto già detto nella prima parte: girando su vari siti di programmazione (tipo l'ottimo stackoverflow, per intenderci), si nota che anche programmatori "scafati" hanno, a volte, dubbi su concetti di base (e chi non ne ha mai avuti scagli la prima pietra!) e poi, più in generale, la lettura di "un altra campana tecnica" può risultare sempre interessante, dando per scontato che la "curiosità tecnologica" è una dote che dovrebbe (anzi, deve!) essere sempre presente negli appassionati di programmazione. Che poi, se uno questa curiosità non ce l'ha... va beh, meglio fermarsi qui, No Comment...) 

Veniamo al dunque: dopo aver visto i primi tre misteri e cioè 1.Il mistero delle funzioni con un parametro array, 2.Il mistero delle funzioni che restituiscono una stringa letterale e 3.Il mistero delle funzioni che restituiscono un char pointer direi che il quarto mistero è quasi scontato, ed è:

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

// prototipi locali
void *myMemcpy(void *dest, const void *src, size_t len);
void *myMemcpyMalloc(void *dest, const void *src, size_t len);

// funzione main
int main(int argc, char *argv[])
{
    // eseguo myMemcpy() e mostro il risultato
    char *src1 = "pluto";
    char dest1[32];
    myMemcpy(dest1, src1, strlen(src1) + 1);
    dest1[strlen(src1)] = 0;    // ricordarsi del terminatore se copiamo una stringa
    printf("myMemcpy(): dest = %s\n", dest1);

    // eseguo myMemcpyMalloc() e mostro il risultato
    char *src2 = "minnie";
    char *dest2 = myMemcpyMalloc(dest2, src2, strlen(src2) + 1);
    dest2[strlen(src2)] = 0;    // ricordarsi del terminatore se copiamo una stringa
    printf("myMemcpyMalloc(): dest = %s\n", dest2);
    free(dest2);                // ricordarsi di questa free()

    return 0;
}

// myMemcpy - versione semplificata della memcpy() della libc
void *myMemcpy(void *dest, const void *src, size_t len)
{
    // salvo localmente dest e src ed eseguo un recast a char*
    char *my_dest = (char *)dest;
    const char *my_src  = (const char *)src;

    // copio la copia locale di src sulla copia locale di dest (copio solo <len> caratteri)
    while (len--)
        *my_dest++ = *my_src++;

    // ritorno il dest salvato
    return dest;
}

// myMemcpyMalloc - versione con malloc interna di myMemcpy()
void *myMemcpyMalloc(void *dest, const void *src, size_t len)
{
    // alloco dest (N.B.: la free() la deve fare il chiamante della myMemcpyMalloc())
    dest = malloc(len);

    // salvo localmente dest e src ed eseguo un recast a char*
    char *my_dest = (char *)dest;
    const char *my_src  = (const char *)src;

    // copio la copia locale di src sulla copia locale di dest (copio solo <len> caratteri)
    while (len--)
        *my_dest++ = *my_src++;

    // ritorno il dest salvato
    return dest;
}
Anche in questo caso il meccanismo è semplice e quasi identico a quello della strncpy() mostrato nella parte 1 dell'articolo. Senza entrare di nuovo nei dettagli (per non ripeterci, e poi il codice è ben commentato), notiamo che la unica differenza vera è che la copia viene eseguita usando copie locali di src e dest "ricastate" a char: perché? La riposta è semplice: questo tipo di funzioni accettano parametri generici (grazie all'uso di void*), quindi si possono passare pointer a strutture, array, ecc. Ma poi, internamente, la copia si fa byte-a-byte, per cui si riconduce tutto al (magico) tipo char*. E, ovviamente, si ritorna sempre l'argomento dest originale, per i motivi già descritti sopra. E anche in questo caso ho pensato di aggiungere una memcpy() poco canonica, con allocazione interna, per la quale valgono tutte le considerazione fatte per la myStrncpyMalloc() già descritta.

Notare che, per coerenza con gli esempi della prima parte del post (e anche per mostrare con semplicità un risultato pratico) ho usato le due myMemcpy() per copiare una stringa (non è un uso classico, ma chi lo vieta?). Ovviamente ho provveduto, a livello main(), ad aggiungere un string terminator, visto che la myMemcpy() a differenza della myStrncpy() già vista, non può (anzi, non deve) pensare a eventuali terminatori di stringa già che è una funzione generica di copia di memoria.

E adesso possiamo cambiare un po' argomento (basta con 'ste funzioni di copia!) e possiamo passare, per esempio a:

5) il mistero delle funzioni con vettori di pointer
Come possiamo generare e maneggiare un vettore di pointer (o meglio: pointer-to-pointers, per gli amici star-star) usando funzioni? Vediamo un esempio semplice semplice, con il solito main() per mostrare i risultati:
#include <stdio.h>
#include <stdlib.h>

#define N_ELEMS 3   // numero di elementi da allocare nel vettore

// una semplice struttura di dati
typedef struct {
    int  id;
    char name[32];
} s_data;

// prototipi locali
s_data **allocVect(s_data **dest, size_t n_elems);
void   freeVect(s_data **dest, size_t n_elems);

// funzione main
int main(int argc, char *argv[])
{
    // alloco il vettore per il test
    s_data **datalist = allocVect(datalist, N_ELEMS);

    // scorro il vettore e mostro il contenuto
    for (int i = 0; i < N_ELEMS; i++)
        printf("allocVect(): datalist[%d]: id=%d name=%s\n",
               i, datalist[i]->id, datalist[i]->name);

    // libero il vettore
    freeVect(datalist, N_ELEMS);

    return 0;
}

// allocVect - alloca un vettore di pointer
s_data **allocVect(s_data **dest, size_t n_elems)
{
    // alloco il pointer-to-pointers
    dest = malloc(n_elems * sizeof(s_data));

    // alloco i pointer
    for (int i = 0; i < n_elems; i++) {
        dest[i] = malloc(sizeof(s_data));

        // questa parte è solo un esempio per mettere dei dati da mostrare nel main()
        dest[i]->id = i;
        snprintf(dest[i]->name, sizeof(dest[i]->name), "name_%d", i);
    }

    // ritorna il pointer-to-pointers
    return dest;
}

// freeVect - libera un vettore di pointer
void freeVect(s_data **dest, size_t n_elems)
{
    // scorro il vettore e libero ogni pointer
    for (int i = 0; i < n_elems; i++)
        free(dest[i]);

    // libera il pointer-to-pointers
    free(dest);
}
Avete visto? Ho scritto due semplicissime funzioni per allocare e liberare un vettore di puntatori (star-star), e la chiave del funzionamento (ben commentata, direi) è la seguente: quando abbiamo un vettore di pointer bisogna creare prima il pointer-to-pointers base (con malloc()) e poi bisogna creare i singoli puntatori del vettore (sempre con malloc()). E quando si libera il vettore bisogna, ovviamente, seguire l'ordine inverso (prima i puntatori singoli e poi il pointer-to-pointers principale). nell'esempio appena mostrato ho aggiunto nel loop di creazione una inserzione di dati, per avere qualcosa da mostrare nel main(): compilatelo, vi assicuro che funziona...

Notare che in questo caso il meccanismo di passaggio e uso degli argomenti è quello visto negli esempi "non canonici" visti nei precedenti misteri: si passa un argomento spurio (un dest non allocato) e lo si restituisce allocato al chiamante. Semplice, no?

E dopo questo esempio credo che siamo pronti per l'ultimo mistero di questa seconda e (per il momento) ultima parte dell'articolo:

6) il mistero delle funzioni per maneggiare linked-list
Le linked-list sono uno dei grandi misteri del C, eppure sono utilissime, e sono anche molto semplici da usare! Vediamo un esempio veramente elementare:
#include <stdio.h>
#include <stdlib.h>

#define N_NODES 3   // numero di nodi nella linked list

// nodo di una linked list singola con campo dati
typedef struct snode {
    int data;
    struct snode *next;
} node_t;

// prototipi locali
void addNode(node_t **head, int data);

// funzione main
int main(int argc, char *argv[])
{
    // init lista vuota e inserisce con addNode() N_NODES nodi con data = indice loop
    node_t *head = NULL;
    for (int i = 1; i <= N_NODES; i++)
        addNode(&head, i);

    // scorre la lista e stampa i valori (compresi gli indirizzi)
    node_t *my_head = head;
    printf("my_head=%p\n", my_head);
    while (my_head) {
        printf("data=%d (my_head=%p next=%p)\n", my_head->data, my_head, my_head->next);
        my_head = my_head->next ;
    }

    return 0;
}

// addNode - alloca in testa a una lista un node con dati e puntatore al prossimo elemento
void addNode(node_t **head, int data)
{
    // alloca un nuovo node
    node_t *node = malloc(sizeof(node_t));
    node->data = data;
    node->next = *head;

    // assegna head lista al nuovo node
    *head = node;
}
Cosa dire? più elementare di così! Eppure funziona... Come si evince dagli abbondanti commenti per fare una linked-list basta creare un punto di inizio (un "head") e aggiungere nodi con una semplicissima funzione che ho chiamato (in modo veramente originale) addNode(). Il segreto sta nella struttura del "nodo", che deve sempre contenere un puntatore al nodo successivo (ma va? Sarà mica per questo che si chiamano linked-list?). Ovviamente l'esempio fornito è abbastanza limitato, e mancano le funzioni per cancellare nodi, per appendere (invece di aggiungere) nodi, oppure per gestire la lista in modo doppio (double-linked-list), ecc., ecc. Ma sulla (solida) base fornita è possibile costruire, in modo relativamente semplice, tutte le estensioni che vogliamo.

Una ultima cosa: notare che il main() mostra, a titolo di esempio, il campo data dei nodi aggiunti insieme agli indirizzi del nodo corrente e del prossimo, e questo ci aiuta a vedere in pratica come funziona il meccanismo.

Beh, direi che con i tre misteri di oggi abbiamo svelato ulteriori dettagli arcani delle funzioni C. Ne rimangono ancora molti, ma per il momento ci prenderemo una pausa, perché sento già friggere molti neuroni e non vorrei creare danni irreversibili...

Ciao e al prossimo post!

Nessun commento:

Posta un commento