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.

venerdì 31 agosto 2012

return, o non return, questo è il dilemma
come strutturare il codice in C

Aveva ragione lui, a volte è difficile prendere delle decisioni esistenziali, e oggi ve ne propongo una io: singolo o multiplo punto di uscita ? Va bene, voi direte, "ci sono cose più importanti a cui pensare". Certamente, ma qui si tratta di stile, e, come già fatto notare qui, lo stile è importante. Del resto questa decisione da prendere non è una mia invenzione, ne ha parlato molto prima di me uno molto più bravo di me, anche se già ai tempi c'era chi non era completamente d'accordo.

Penso che il dilemma sia chiaro: stiamo parlando di Programmazione Strutturata mica di quisquilie. E se qualcuno sta pensando "ma la programmazione strutturata è superata", oppure "con la OOP l'approccio è completamente diverso", lo fermo subito. Qui si parla del C, il padre (o perlomeno lo zio) di tutti i linguaggi moderni, e il C è un linguaggio strutturato. Punto e basta. E se pensate che il C e la Programmazione Strutturata sono superati vi conviene prendervi una pausa di riflessione...

E allora ? Va beh, vediamo un esempio così ci chiariamo le idee. Scriviamo una funzione che apre un file, estrae la prima linea, processa i dati della linea ed esce. Come sempre si potrebbe scrivere in varie migliaia di modi diversi, ma noi isoliamo i due che ci servono per l'esempio (beh, isoliamo anche un terzo modo, ma quello è per finire in bellezza il post). Vediamo prima il tipo Single-Exit:
int readFile(
    char *path)
{
    char *line = NULL;
    size_t len = 0;
    int retcode;

    // apro il file in argomento
    FILE *fp;
    if (fp = fopen(path, "r")) {
        // leggo il file
        if (getline(&line, &len, fp) != -1) {
            // test contenuto file
            if (!strncmp(line, "output=", 7)) {
                // processa dati
                // ...

                retcode = 0;    // proc. OK: set retcode=OK
            }
            else
                retcode = -3;   // str err: set retcode=err

            free(line);     // libera risorse
        }
        else
            retcode = -2;   // getline err: set retcode=err

        fclose(fp);     // libera risorse
    }
    else
        retcode = -1;   // fopen err: set retcode=err

    // esce con il retcode settato
    return retcode;
} 

E adesso possiamo passare a analizzare il suo perfetto equivalente in modalità Multiple-Exit, ossia:
int readFile(
    char *path)
{
    char *line = NULL;
    size_t len = 0;

    // apro il file in argomento
    FILE *fp;
    if ((fp = fopen(path, "r")) == NULL) {
        // esce con errore
        return -1;
    }

    // leggo il file
    if (getline(&line, &len, fp) == -1) {
        // libera risorse ed esce con errore
        fclose(fp);
        return -2;
    }

    // test contenuto file
    if (strncmp(line, "output=", 7)) {
        // libera risorse ed esce con errore
        fclose(fp);
        free(line);
        return -3;
    }

    // processa dati
    // ...

    // proc. OK: libera risorse ed esce con OK
    fclose(fp);
    free(line);
    return 0;
} 

Quale è il tipo migliore ? Beh, per me (e lui) è il tipo Single-Exit, ma, c'è da dire che il partito del Multiple-Exit è numeroso e formato (anche) da gente molto preparata.

E allora dove sta la verità, forse nel mezzo ? No, dire sempre che verità sta nel mezzo è la scusa degli indecisi. Io scelgo la soluzione con lo stile migliore, la più bella e facile da leggere, e, aggiungo, quella che ti dà un controllo maggiore mentre la scrivi: nel tipo Single-Exit è più difficile dimenticarsi di liberare le risorse giuste nel momento giusto (pensate a un programma più complesso, non all'esempio). E, inoltre, il tipo Single-Exit è anche più facile da leggere e debuggare (provare per credere, ma sempre con un caso più complesso).

Potrei fare un eccezione: quando con il tipo Single-Exit il livello di annidamento diventa troppo alto forse si potrebbe fare un ibrido: mettere i test basici (tipo "il parametro passato è nullo ?") all'inizio con i relativi return, e poi seguire in modo Single-Exit (ma, se alla fine il codice risultante non vi convince, magari è meglio se spezzate in due la funzione).

Comunque, sono meno radicale di quello che sembra: tra il codice che ho scritto nei miei trascorsi c'è (basta guardare qui) del codice Multiple-Exit o ibrido. Il fatto è che bisogna essere un po' elastici: programmare (bene) è un'arte, e troppe restrizioni non giovano all'espressività.

Come preannunciato chiudiamo in bellezza con il terzo tipo: anche la funzione seguente funziona, ed è un perfetto equivalente delle precedenti: 
int readFile3(
    char *path)
{
    char *line = NULL;
    size_t len = 0;

    // faccio tutto in una botta sola
    FILE *fp;
    if ((fp = fopen(path, "r")) && (getline(&line, &len, fp) != -1) &&
            !strncmp(line, "output=", 7)) {

        // processa dati
        // ...

        // dati processati: libera risorse ed esce con OK
        fclose(fp);
        free(line);
        return 0;
    }
    else {
        // libera risorse ed esce con errore
        if (fp)
            fclose(fp);

        if (line)
            free(line);

        return -1;
    }
}

Ecco, se vi viene in mente di scrivere una funzione in quest'ultima maniera (magari mettendo ancora più test nello stesso if) rilassatevi, prendetevi un periodo di riposo, leggete qualche libro interessante e poi riprovateci con la testa finalmente sgombra da cattivi pensieri: magari (spero) non vi verrà più in mente di scriverla così. 

Ciao e al prossimo post.

giovedì 23 agosto 2012

No Comment
come scrivere i commenti nel C

Se siete di quelli che "Non scrivo commenti. Sono una perdita di tempo" fermatevi qui. Tanto non riuscirei a convincervi (e neanche ci tengo).

Per tutti gli altri: il commento è un vostro amico fedele, non vi abbandonerà mai. Il commento è l'ultima barriera tra il capire e il non capire "cosa avevo in testa quando ho scritto quel programma un anno fa ?". Il commento è la vostra dimostrazione di rispetto per i colleghi di lavoro: prima o poi, qualcuno di loro metterà le mani sul vostro Software, e il numero di maledizioni che vi manderà sarà inversamente proporzionale al numero (e alla qualità) dei commenti che avete scritto.

Certo, ci sono varie maniere di commentare il codice: si va dal "Absolute No Comment" al "auto-commentante con aggiunta di commenti" (beh, questo è un po' esagerato...), con tutte le sfumature intermedie possibili...

Facciamo una piccola dimostrazione: esaminiamo tre maniere si scrivere una funzione di lettura di un dispositivo di controllo industriale (si tratta, nel Caso 3, di codice reale che ho scritto alcuni anni fa: per questo esempio ho solo cambiato alcuni nomi). Si noti che i tre codici sono praticamente identici, anche se la lettura da tutt'altra impressione...

Caso 1: codice Absolute No Comment

int smsg(S_dev *p)
{
    if (p->msg) {
        p->btx[0]=p->msg;
        if (!sndch(p->ch,(char *)p->btx,(size_t)1))
            return(-1);
        p->msg=0;
    }
    return(0);
}


come noterete non ci sono commenti, il codice è (a dir poco) criptico, e si risparmia su tutto: nomi, spazi, linee: non si sa mai che qualcuno potrebbe capire cosa c'è scritto. Per maniaci della segretezza.

Caso 2: codice auto-commentante

int sendMessageToDevDummy(
    S_dev_dummy *p_devdum)
{
    if (p_devdum->msg_to_send != 0) {
        p_devdum->buffer_tx[0] = p_devdum->msg_to_send;

        if (!send_char_to_dev(p_devdum->channel_tx, (char*)p_devdum->buffer_tx, (size_t)1))
            return(-1);

        p_devdum->msg_to_send = 0;
    }

    return(0);
}


come noterete non ci sono commenti, ma il codice è auto-esplicativo e non si va al risparmio. I nomi sono scelti per la massima chiarezza. Per chi non ama i commenti ma vuole essere chiaro a tutti i costi.

Caso 3: codice chiaro e commentato

/* sendMessage()
 * invia un messaggio al dispositivo dummy
 */
int sendMessage(
    S_dev_dummy *dev)
{
    // attendo i messaggi da spedire
    if (dev->msg_2snd) {
        // copio il messaggio nel buffer di trasmissione
        dev->buf_tx[0] = dev->msg_2snd;

        // spedisco il messaggio
        if (! send_char_2dev(dev->chan_tx, (char *)dev->buf_tx, (size_t)1))
            return(-1);

        // libero il semaforo di trasmissione
        dev->msg_2snd = 0;
    }

    return(0);
}


come noterete ci sono una intestazione (che si dovrebbe espandere per le funzioni importanti) e molti brevi commenti. Il codice non è auto-esplicativo ma è sufficientemente chiaro. Per comment-lovers.

Inutile dirlo: io appartengo alla scuola comment-lovers. Però rispetto anche i seguaci del codice auto-commentante. Sui signori del Absolute No Comment l'unica cosa che posso dire è... No Comment. Comunque, se vi può interessare, il Caso 1 non è una forzatura, e ho visto anche di peggio, non fatemi parlare...

Ciao e al prossimo post.

mercoledì 15 agosto 2012

Sei sicuro delle tue stringhe ?
come trattare le stringhe nel C

Viviamo in un mondo reale. In un mondo ideale non ci sarebbe bisogno di testare il ritorno di una malloc(), prima di usare la memoria gentilmente concessaci. Anzi, non ci sarebbe neanche bisogno della malloc(). In un mondo ideale non ci sarebbe bisogno di testare la sicurezza di una applicazione, perché nessuno cercherebbe mai di attaccare, con metodi svariati, il nostro software per cercare falle strane.

Ma, viviamo in un mondo reale. E allora, visto che il più diffuso attacco craker si basa sulla nota tecnica del buffer overflow, dovremmo tenere sotto controllo tutte le stringhe di input di una applicazione, sia che vengano da un altro programma, sia che vengano direttamente da attività interattive. Ok, sembra molto complicato e dispendioso, ma se prendiamo l'abitudine di usare alcune semplici funzioni (scritte ad hoc) della nostra personale libc, il risultato finale non sarà, poi cosi antiestetico e complicato da realizzare.

Un piccolo esempio: tutte le stringhe provenienti da mondo esterno le filtriamo con una funzione che potremmo chiamare strSecure():

/* strSecure()
 * trattamento di sicurezza (buffer overflow) della string <src>: ritorna una stringa di 

 * max <len> char con terminatore
 */
char *strSecure(
    char         *dest,
    const char *src,
    size_t        len)
{
    // trattamento di sicurezza (buffer overflow) della string <src>
    if (! memccpy(dest, src, '\0', len))
        dest[len - 1] = '\0';
       
    // ritorna una stringa di max <len> char con terminatore
    return(dest);
}


Ossia: copiamo la stringa e, se memccpy() non trova il terminatore, lo forziamo noi. In pratica si potrebbe usare, più o meno, così (sarà un esempio un po' sintetico, lo ammetto...):

main()
{
    ...
    // legge input string
    char *inputstr = ...
    ...

    // usa input string
    char my_inputstr[80];
    sprintf(buf, "input string is: %s", strSecure(my_inputstr, inputstr, sizeof(my_inputstr));
    ...
}


Ah, dimenticavo: se volete vivere bene e sicuri, vestitevi pesanti in inverno, leggeri in estate, e, soprattutto, non usate mai la gets().

Ciao e al prossimo post.

lunedì 13 agosto 2012

Mamma, ho perso l'atoi()

Vorrei proporvi un piccolo test: aprite un editor e, senza consultare google, manuali, o programmi archiviati, scrivete una funzione atoi(). Non vi spiego cos'è, perché se il nome non vi suona avete già fallito il test. Nel caso che la memoria non vi abbia tradito, dovreste scrivere un piccolo main() che richiami la vostra funzione passandogli un parametro fisso e, ad esempio, scriva a video il risultato. Non perdete tempo a fare un programma interattivo che gestisca l'input: l'obiettivo è solo scrivere una atoi() funzionante.

Bene, se ci avete messo più di 5 minuti (vabbeh, sarò generoso: 10 minuti), allora avete bisogno di una sana rinfrescata delle basi del C. Non sorprendetevi, magari uno usa il C spesso, magari scrive anche cose complicate, però, grazie al cut&paste di codice scritto precedentemente, oppure di esempi trovati in rete (grazie google !) è possibile che uno, alla lunga, si dimentichi di come si fanno le cose semplici.

Nessuno di quelli che non c'è l'hanno fatta si offenda: siete in buona compagnia. Perché pensate che vi sto proponendo proprio questo test? Beh, alcuni anni fa, mi hanno dato un foglio e una penna (ancora più difficile! Neanche un PC con un compilatore per provare) e mi hanno detto "Scrivi una atoi()". L'ho scritta e l'ho sbagliata clamorosamente. La sera stessa ho tirato fuori dalla libreria il K&R e ho cominciato a rileggerlo, dalla prima all'ultima pagina (erano molti, molti, molti anni che non lo rileggevo interamente): è stata una gran sorpresa, c'erano argomenti che non ricordavo neanche che venivano trattati, e ho riscoperto parti dimenticate del C che per (cattiva) abitudine non usavo da anni.

Ora che ho preso l'abitudine di rinfrescarmi la memoria periodicamente, il test non lo sbaglio più. Ehm, ovviamente prima di scrivere questo post mi sono obbligato a fare un test a sorpresa dell'atoi(), e, fidatevi, questa volta c'è l'ho fatta (del resto, se non ci fossi riuscito, non avrei avuto il coraggio di scrivere questo post...).

Ciao e al prossimo post.

sabato 11 agosto 2012

Oh my, my, strncpy()
come usare la strncpy() in C

In attesa di qualche idea per un post più interessante (e sicuramente più lungo da preparare) mi soffermerò un po' sulla libc e, in particolare, sulla strncpy(), anche se, in realtà, la scelta della funzione è solo un pretesto per dare alcuni suggerimenti di stile, come vedrete (se avete un vuoto di memoria con la libc, clickate sul nome della funzione).

Scrivere in C senza usare direttamente le funzioni di libreria della libc è abbastanza inusuale, ma ci sono dei casi in cui, tra le n-mila funzioni disponibili, manca proprio quella che fa al caso tuo (oppure c'è e non te ne sei accorto). Esempio: mi è successo (caso reale, giuro) di dover leggere linee ripetitive da un file e di dover estrarre solo una parte delle linee per copiarlo in un array, ossia, riassumendo:

- abbiamo n-linee del tipo "...id = 123456;" (il numero cambia a ogni linea e, al posto dei puntini si sono altre informazioni).
- il numero puo`essere più o meno lungo: da 1 a 8 caratteri.
- dobbiamo estrarre il numero, trasformarlo in int e copiarlo in un array di int.

Beh, ovviamente ci sono molti modi per farlo, e uno potrebbe essere di identificare dove comincia il numero e poi usare usare la strncpy()... ma il numero non ha lunghezza fissa: allora bisogna contare la lunghezza e poi usare la strncpy(). Oppure usare la strtok(), e chi più ne ha più ne metta. E il codice diventa sempre più lungo e illeggibile: come sarebbe stato bello risolvere tutto con una semplice chiamata alla strcnpy() !

Ma dico io: chi l'ha detto che se nella libc non c'è la funzione che fa al caso mio non posso riscriverla ? Non necessariamente queste funzioni sono oggetti misteriosi e indescrivibili. Ad esempio la strncpy() è una funzione molto semplice, e riscriverla a proprio uso e consumo è facilissimo:

/* myStrncpy()
 * versione speciale della strncpy() que si ferma al primo ";" e copia max (n-1) char 

 * assicurando un "\0" finale
 */
char *myStrncpy(
    char         *dest,
    const char *src,
    size_t       n)
{
    size_t i;

    // copia src in dest (con stop a <n - 1> chars o al primo "\0" o al primo ";")
    for (i = 0; i < (n - 1) && src[i] != '\0' && src[i] != ';'; i++)
        dest[i] = src[i];

    // fill con "\0" i chars restanti
    for ( ; i < n; i++)
        dest[i] = '\0';

    // ritorna un pointer a dest
    return dest;
}


E così, il flusso principale del programma fa una sola chiamata alla myStrncpy() senza fronzoli strani, salvando eleganza e leggibilità. In un altro file ci mettiamo la myStrncpy(), pronta per essere riutilizzata in altri progetti (farà parte della nostra, personale, libc).

Ciao e al prossimo post.

venerdì 10 agosto 2012

hello, world

Beh, il primo post di un blog sull'arte di programmare in C non poteva che intitolarsi "hello, world" (ehm... "salve, mondo" in Italiano, ma preferisco la versione originale). Come ben sapranno tutti quelli che, come me, si sono avvicinati al mondo del C attraverso il mitico K&R, nel cap.1 viene illustrato il primo esempio di programma in C:

int main()
{
     printf("hello, world\n");
}

evidentemente non si poteva che cominciare da qui. Ok, il titolo che vedete in alto "L'arte della programmazione in C" è, forse, un po' roboante, ma questo blog è diretto a tutti quelli che considerano la programmazione un'arte, non semplicemente un lavoro, un hobby o un male necessario (vi assicuro che conosco gente che la considera così).  Scrivere Software è un piacere. Un programma non solo deve funzionare bene ed essere efficiente (questo si dà per scontato), ma deve essere anche  bello ed elegante da leggere, comprensibile e facile da manutenere, sia per l'autore che per eventuali lettori futuri.

Questo blog non è per chi deve imparare a programmare in C (in rete ci sono molti siti più adatti), ma per chi lo conosce già e si diletta a scrivere in C. Man mano che mi verranno in mente argomenti interessanti pubblicherò possibili implementazioni (artistiche, spero), e chi vorrà commentare proponendo migliorie o realizzazioni diverse, è ben accetto.

Ciao e al prossimo post.