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.

martedì 27 febbraio 2024

Librerie header-only? Forse!
come scrivere una libreria header-only in C

Holland March: Alla fine nessuno si è fatto male.
Jackson Healy: A me sembra di sì...
Holland March: Nel senso che sono morti in fretta, non è che si sono fatti male.

Ebbene si, oggi parleremo di un argomento abbastanza inusuale e poco conosciuto del C (almeno per quello che mi riguarda): le librerie header-only. Come vedremo più avanti questo è un argomento un po' controverso e (forse) di dubbia utilità, ma per evitare gli strali dei fan (ce ne sono) di questo tipo di librerie ho deciso che questo sarà un articolo del tipo "Forse!" invece che un "No, grazie!" (a proposito, di "No, grazie!" ne ho scritti un po', li potete trovare quiqui, quiqui, qui e qui, mentre un esempio di "Forse!", o meglio, era uno "Scusate", lo trovate qui). L'argomento dell'articolo, tra il serio e il faceto, si intona con il bel film The Nice Guys del bravo Shane Black, un film brillante di quelli che Hollywood anticamente sfornava a ripetizione, ma che ora non sanno quasi più fare...

...secondo me è una libreria header-only...

E cosa sono le queste librerie header-only? Ok, partiamo dalle basi (Linux, eh! Ma anche in altri S.O. funziona più o meno allo stesso modo): qui stiamo per parlare di librerie di sviluppo (su Linux, famiglia Deb, sono i pacchetti *-dev), che sono quelle che servono per realizzare una applicazione. Normalmente (anzi canonicamente) una libreria di sviluppo si distribuisce (semplificando un po') attraverso due file:

  • Un header-file (un *.h) che contiene i prototipi delle funzioni disponibili più la definizione di eventuali strutture dati tipiche della libreria: questo permette di scrivere e compilare una applicazione che usa quello che mette a disposizione la libreria.
  • Un lib-file (un *.a o un *.so), che è una versione compilata dei file di implementazione (i *.c) che compongono la libreria. Questo lib-file può avere formato a link statico (*.a) o dinamico (*.so): grazie a questo si può linkare ed eseguire la applicazione che usa la libreria. Quindi quando si distribuisce la applicazione in formato eseguibile bisogna anche distribuire la libreria associata.
  • La libreria da distribuire avrà varie versioni in base a tipo/versione del S.O. della macchina su cui si vuole installare e usare.

Come si può intuire (e se no che lo scrivo a fare questo articolo?) c'è anche una maniera "non canonica" di distribuzione, ed è proprio quella delle librerie header-only:

  • Si distribuisce un solo file, un header-file un po' strano che contiene prototipi, strutture dati e anche implementazioni! Questo header si deve includere in tutti i file del progetto che usano funzioni e/o strutture dati della libreria.
  • Si compila e via! Si può eseguire l'applicazione linkando solo le (eventuali) altre librerie necessarie all'applicazione. Quindi quando si distribuisce la applicazione in formato eseguibile non c'è bisogno di distribuire anche la libreria associata.
  • La portabilità è buona: una libreria header-only è, a tutti gli effetti, un file sorgente, quindi se si riesce a compilare sarà perfettamente compatibile con la macchina che stiamo usando (eventuali problemi verranno dopo, per distribuire la sola applicazione precompilata ed eseguibile).
  • Notare che alcune delle librerie header-only disponibili in rete usano alcuni trucchi (basati sulla definizione di alcune macro ad-hoc) per far si che uno solo dei file *.c (del progetto che userà la libreria) includa la parte di implementazione del header-file, mentre gli altri *.c includeranno solo la lista dei prototipi contenuta nell'header-file. In questo caso si dovrebbe parlare di librerie pseudo-header-only, che non sono l'argomento di questo articolo: qui parleremo solo di quelle "vere".

Apparentemente questo metodo di distribuzione è interessante, ma non è oro tutto quello che luccica: in letteratura tecnica sono riportati alcuni difetti:

  • In un grande progetto con molti sorgenti *.c che includono la libreria header-only una modifica a quest'ultima provocherà un notevole allargamento dei tempi di compilazione (l'header  verrà ricompilato n-volte). Con la potenza delle macchine attuali questo potrebbe non essere un gran problema (anche se non bisogna sottovalutarlo: alcuni progetti sono veramente grandi e comprendono migliaia di sorgenti).
  • Per evitare gli ovvi errori di ridefinizione in compilazione si fa grande uso (come vedremo più avanti) di storage classes di tipo static e/o static inline: questo provoca una crescita notevole del codice macchina generato, un problema difficilmente ottimizzabile dal compilatore.

Due problemi da niente, no?

E vabbé, è ora di dare un esempio reale (ultra-semplificato) di un progetto che usa una libreria header-only; il progetto include:

  • mylib.h: la libreria header-only (è una "vera", senza i trucchi citati sopra).
  • test1.c: un sorgente C che definisce una funzione fun1() che usa internamente una funzione della libreria mylib.h.
  • test2.c: un sorgente C che definisce una funzione fun2() che usa internamente una funzione della libreria mylib.h.
  • test.c: un sorgente con un main() che chiama le funzioni definite in test1.c e test2.c.

Vai col codice!

// mylib.h - una libreria header-only
#include <stdio.h>

#ifdef STATICINLINE
#define MY_API static inline
#define MY_API_STR "static inline api"
#elif defined STATIC
#define MY_API static
#define MY_API_STR "static api"
#elif defined INLINE
#define MY_API inline
#define MY_API_STR "inline api"
#elif defined EXTERN
#define MY_API extern
#define MY_API_STR "extern api"
#else
#define MY_API
#define MY_API_STR "api"
#endif

// libfun1 - una funzione generica della libreria
MY_API void libfun1(void)
{
printf("%s: sono %s\n", MY_API_STR, __func__);
}

// libfun2 - una funzione generica della libreria
MY_API void libfun2(void)
{
printf("%s: sono %s\n", MY_API_STR, __func__);
}
/ test1.c - modulo per il test della libreria header-only mylib
#include "mylib.h"

void fun1(void)
{
// chiamo una funzione della libreria header-only mylib
libfun1();
}
/ test2.c - modulo per il test della libreria header-only mylib
#include "mylib.h"

void fun2(void)
{
// chiamo una funzione della libreria header-only mylib
libfun2();
}
/ test.c - main di test della libreria header-only mylib

// test - funzione main
int main(void)
{
extern void fun1(void);
extern void fun2(void);

// chiamo le funzioni dei moduli test1 e test2
fun1();
fun2();

return 0;
}

Come sempre il codice è ben commentato, però in questo caso qualche spiegazione è doverosa: a parte le espressioni condizionali iniziali (che vedremo più avanti) il meccanismo di avere prototipi e definizioni in mylib.h è semplice e abbastanza chiaro, no ? Quindi visto che test1.c e test2.c includono mylib.h possono chiamare, rispettivamente, libfun1() e libfun2() internamente alle funzioni globali fun1() e fun2(). Dopodiché test.c, che non ha bisogno di includere mylib.h, chiama le funzioni globali fun1() e fun2(): tutto abbastanza lineare.

Ma l'inclusione in più sorgenti di uno strano header-file con prototipi e implementazioni non è triviale, e quindi bisogna scegliere accuratamente le storage classes delle funzioni, perché gli errori di ridefinizione sono dietro l'angolo. Il meccanismo che permette il funzionamento è, come anticipato sopra, quello che si vede nelle prime linee di mylib.h, dove ho messo alcune espressioni condizionali che permettono di testare e mostrare le varie maniere operative. Poi, una volta capito il meccanismo, mylib.h si può semplificare togliendo tutti i condizionali e lasciando solo il tipo di storage classes scelto.

Vediamo, allora, cosa succede compilando (e, se possibile, eseguendo) la nostra applicazione:

aldo@Linux $ gcc test.c test1.c test2.c -o test -DSTATICINLINE
aldo@Linux $ ./test
static inline api: sono libfun1
static inline api: sono libfun2

aldo@Linux $ gcc test.c test1.c test2.c -o test -DSTATIC
aldo@Linux $ ./test
static api: sono libfun1
static api: sono libfun2

aldo@Linux $ gcc test.c test1.c test2.c -o test -DINLINE
/usr/bin/ld: /tmp/ccdLwOwW.o: in function `fun1':
test1.c:(.text+0x9): undefined reference to `libfun1'
/usr/bin/ld: /tmp/ccK47RcM.o: in function `fun2':
test2.c:(.text+0x9): undefined reference to `libfun2'
collect2: error: ld returned 1 exit status

aldo@Linux $ gcc test.c test1.c test2.c -o test -DEXTERN
/usr/bin/ld: /tmp/ccNPBHJP.o: in function `libfun1':
test2.c:(.text+0x0): multiple definition of `libfun1'; /tmp/cczGXRP8.o:test1.c:(.text+0x0): first defined here
/usr/bin/ld: /tmp/ccNPBHJP.o: in function `libfun2':
test2.c:(.text+0x33): multiple definition of `libfun2'; /tmp/cczGXRP8.o:test1.c:(.text+0x33): first defined here
collect2: error: ld returned 1 exit status

aldo@Linux $ gcc test.c test1.c test2.c -o test
/usr/bin/ld: /tmp/cc5t0uT9.o: in function `libfun1':
test2.c:(.text+0x0): multiple definition of `libfun1'; /tmp/ccLox6Tj.o:test1.c:(.text+0x0): first defined here
/usr/bin/ld: /tmp/cc5t0uT9.o: in function `libfun2':
test2.c:(.text+0x33): multiple definition of `libfun2'; /tmp/ccLox6Tj.o:test1.c:(.text+0x33): first defined here
collect2: error: ld returned 1 exit status

Come si nota il programma si compila ed esegue regolarmente solo nei modi STATICINLINE e STATIC: la parola chiave è, in entrambi i casi static, perché definendo così le funzioni la definizione resta limitata al singolo file che include mylib.h, quindi non ci sarà nessun errore di ridefinizione (ma si cade nel problema descritto sopra: una crescita del codice macchina risultante). Aggiungendo a static anche il function specifier  inline, il risultato non cambia: funziona bene e, inoltre, il compilatore cerca, se possibile, di "inlineare" la funzione (e anche in questo caso si cade nello stesso problema descritto sopra, però l'eseguibile potrebbe essere più efficiente). Notare che le funzioni mostrano anche il tipo di interfaccia MY_API in uso, ottenuta dalla macro MY_API_STR di mylib.h.

E cosa succede compilando coi modi INLINE, EXTERN e "senza modo"? Succede che ci sono errori di compilazione, quindi non possiamo neanche eseguire. Nel modo INLINE (senza static) notiamo che la funzione non viene resa disponibile al linker, e quindi abbiamo degli errori del tipo "undefined reference to libfun1": il linker ha bisogno, per questo tipo di librerie della parola magica static. Nei modi EXTERN e "senza modo" abbiamo, invece, degli errori del tipo "multiple definition of libfun1", che indicano che, chiedendo un linkaggio di tipo extern (o senza tipo) e fornendo poi la funzione ogni volta che si include mylib.h si verifica il problema di avere multiple definizioni. Come previsto.

È tutto chiaro? Spero di si.

E qui ci sta bene aggiungere qualche considerazione personale, perché non ho ancora detto se considero buone o cattive le librerie header-only (magari l'ho fatto intuire, però): devo ammettere che non mi piacciono, perché le considero una forzatura del linguaggio: il fatto che, con le dovute accortezze, si riesca a farle digerire al compilatore e al linker, non significa che sia una buona idea usarle.

E poi inserire la definizione di una funzione dentro un header-file è più roba da C++ (il lato oscuro della forza): li è abbastanza usuale che in un header-file ci sia, all'interno della definizione di una classe, anche il codice dei metodi (questo è Ok ma non mi piace: io preferisco scriverlo nel file di implementazione della classe); per non parlare poi dei (famigerati) template, dove è addirittura obbligatorio (o perlomeno molto raccomandabile) avere "tutto" (class template  e codice dei metodi) in un solo header-file. Anche per questo i compilatori C e C++ trattano in maniera un po' differente le storage classes  descritte sopra. Conclusione: se proprio vi piacciono le librerie header-only passate al C++, ah ah ah.

Per oggi può bastare, sono contento di avere scritto un nuovo articolo del tipo "Forse!", perché su alcuni argomenti non è necessario essere troppo radicali... ma su altri si, quindi aspettatevi qualche altro "No, grazie!" in futuro! E non trattenete il respiro nell'attesa, mi raccomando!

Ciao, e al prossimo post!

mercoledì 24 gennaio 2024

Valgrind e vengo da lontano
come usare Valgrind con il C (e C++)

Willy: ...quindi ora, se lei è d'accordo, se lei è d'accordo con me, io farei un articolo bellissimo, complesso, ardito anche, su questo argomento a tutta pagina dal titolo: "Con chi lo facciamo accoppiare il lucertolone del Sudan?... Con chi lo facciamo accoppiare?".
caporedattore: [non risponde e gli chiude la porta in faccia]
Willy: [parlando da solo] ...con la troia de la tu moglie lo facciamo accoppiare...

Il dialogo surreale qui sopra è tratto dal bel Willy Signori e vengo da lontano del compianto Francesco Nuti. Il Willy del film è la rappresentazione del "vero amico", quello che ti aiuta nel momento del bisogno, che ti accompagna in maniera disinteressata (vabbé, nel film Willy è guidato anche dai sensi di colpa, ma nel complesso è un vero "cuore d'oro"). E cosa centra Valgrind in tutto questo? Beh, Valgrind è un vero amico del programmatore C (e C++), uno che ti aiuta a scrivere programmi bug-free a prova di bomba, come vedremo tra poco.

...con chi lo facciamo accoppiare il Valgrind?...

Allora, prima di tutto bisogna spiegare cos'è Valgrind, perché non è detto che tutti lo sappiano. Valgrind è un analizzatore dinamico di comportamento del codice, cioè serve per testare il funzionamento di un programma in condizioni reali (o, perlomeno, il più possibile vicino alla realtà). Tanto per rinfrescare i concetti (nel caso che qualcuno ne abbia bisogno) ricordiamo che il primo passo di analisi si fa con gli analizzatori statici, e cioè:

  1. La analisi statica più statica che c'è è, ovviamente, quella della compilazione: attivando gli opportuni flag per il compilatore e interpretando i Warning proposti (errori, per definizione, non ce ne sono mai, ah ah ah) facendo in maniera che spariscano tutti, possiamo già dire di avere un codice sintatticamente corretto. Io consiglio sempre un alto livello di controllo, che con GCC significa attivare i flag Wall e pedantic.
  2. Si può poi passare il codice con un vero e proprio analizzatore statico, quelli della famiglia lint. Ce ne sono vari, ad esempio per C/C++ io uso l'ottimo Cppcheck; notare che un buon lint va oltre i suoi scopi originari, e può mostrare anche errori di tipo "dinamico", ma solo quelli evidenti anche staticamente (uhm, sembra strano ma è così...).
  3. Sia per il caso 1 che per il caso 2 la analisi deve essere fatta cum grano salis: non è detto che non ci siano casi di falsi positivi del lint o che qualche Warning di compilazione non sia così eccessivo e pedante che è opportuno eliminarlo: GCC in questo caso fornisce per ogni tipo di Warning un flag per ometterlo dai risultati.

E adesso veniamo al dunque: un programma che ha passato i 3 punti sopra potrebbe ancora, magicamente, schiantarsi durante l'uso reale o, ancora peggio, schiantarsi ogni tanto, anzi, per la legge di Murphy potrebbe non schiantarsi mai durante lo sviluppo e il testing e cominciare a dare problemi ogni tanto dopo la distribuzione sulle macchine di produzione (con grande felicità dei clienti...) il che rende molto difficile capire dove è il problema. Questo è dovuto, quasi sempre, a problemi nella gestione della memoria, che pregiudicano il comportamento dinamico.

(...apro una parentesi: in questi casi il programmatore inesperto e/o che non ha il controllo totale del codice scritto ricorre a un debugger per trovare l'errore; ecco, bisognerebbe ricordarsi che, come ho già scritto in un vecchio articolo, il debugger aiuta solo a risolvere problemi facili (e ripetibili), mentre per problemi difficili (o non ripetibili) non serve a nulla, anzi potrebbe dare informazioni fuorvianti. Riepiloghiamo: il debugger aiuta solo a risolvere i problemi semplici, ma questi problemi si possono risolvere, con competenza e un po' di intuito, senza usare il debugger: uhm... è una classica situazione da "gatto che si morde la coda". Chiudo la parentesi...)

E come si procede in questo caso? ma usando un analizzatore dinamico! Uno come Valgrind che analizza il comportamento del programma "mentre sta lavorando", e fornisce dati utilissimi di memoria occupata e non liberata, memoria non inizializzata, ecc. arrivando anche a dettagliare lo stack di chiamata delle istruzioni che hanno generato l'errore, numeri di linea inclusi. Un bell'aiuto, non c'è che dire! Ovviamente anche Valgrind deve essere usato correttamente, per evitare di farsi ingannare da eventuali falsi positivi e, soprattutto, la analisi deve essere effettuata in condizioni reali.

A questo riguardo vi faccio un esempio semplicissimo: supponiamo di analizzare il comportamento di un Server TCP: se lo lanciamo attraverso Valgrind e non gli facciamo fare nulla il risultato sarà: "Il programma è perfetto!", e in questo caso la considerazione successiva sarebbe "E grazie al..."! È evidente che il funzionamento dinamico di un Server TCP si deve analizzare in condizione di forte carico di rete, magari anche molto più alto di quello usuale, e con molteplici tipi di messaggi, perché i problemi (se ci sono) verranno fuori solo durante l'uso intensivo.

E, a questo punto ci vuole un po' di codice: vi propongo un semplicissimo programma pieno di errori, che si vedono a prima vista, ma pensate che questi errori potrebbero essere distribuiti, uno qua, uno la, su n-mila linee di codice scritte su n-file (quindi non più facilmente visibili). Questo programma passa bene la compilazione con tutti i Warning attivati, il Cppcheck trova qualche errore pseudo-dinamico (ma non tutti) e, dulcis in fundo, nell'uso reale apparentemente non si schianta (ma questo dipende un po' dalla fortuna del momento). Vai col codice!

// test.c - test per Valgrind
#include <stdio.h>
#include <stdlib.h>
#include <string.h>

#define LEN_ARRAY 16
#define NUM_STRINGS 8

// test - funzione main
int main(void)
{
// alloco memoria per un array di int di lunghezza LEN_ARRAY
int *myarray = malloc(LEN_ARRAY * sizeof(int));

// errore 1: scrivo una posizione non disponibile (write out-of-bound)
myarray[LEN_ARRAY] = 0;

// errore 2: leggo una posizione non disponibile (read out-of-bound)
printf("myarray[%d] = %d\n", LEN_ARRAY, myarray[LEN_ARRAY]);

// alloco memoria per le stringhe di un array di NUM_STRINGS char pointers
char *strings[NUM_STRINGS];
for (int i = 0; i < NUM_STRINGS; i++) {
strings[i] = malloc(LEN_ARRAY * sizeof(char));
memset(strings[i], 0, LEN_ARRAY * sizeof(char));
}

// errore 3: manca la free(3) delle stringhe e anche di myarray (memory leak)

return 0;
}

Come vedete sono pochissime linee di programma, con ben tre errori che, ripeto, qui si vedono immediatamente, ma se immersi in n-mila linee di codice sono difficili da trovare.

E come si usa Valgrind? Allora bisogna, semplicemente, compilare il programma (con l'opzione di mantenere i simboli, così avremo anche le informazioni dei numeri di linea) ed eseguire attraverso Valgrind. Vediamo, ad esempio cosa succede con GCC:

aldo@Linux $ gcc -g -Wall -pedantic test.c -o test
aldo@Linux $ ./test
myarray[16] = 0
aldo@Linux $ valgrind ./test --leak-check=full
==40693== Memcheck, a memory error detector
==40693== Copyright (C) 2002-2017, and GNU GPL d, by Julian Seward et al.
==40693== Using Valgrind-3.18.1 and LibVEX; rerun with -h for copyright info
==40693== Command: ./test --leak-check=full
==40693==
==40693== Invalid write of size 4
==40693== at 0x1091DA: main (test.c:16)
==40693== Address 0x4a9c080 is 0 bytes after a block of size 64 alloc d
==40693== at 0x4848899: malloc (in /usr/libexec/valgrind/vgpreload_memcheck-amd64-linux.so)
==40693== by 0x1091CD: main (test.c:13)
==40693==
==40693== Invalid read of size 4
==40693== at 0x1091E8: main (test.c:19)
==40693== Address 0x4a9c080 is 0 bytes after a block of size 64 alloc d
==40693== at 0x4848899: malloc (in /usr/libexec/valgrind/vgpreload_memcheck-amd64-linux.so)
==40693== by 0x1091CD: main (test.c:13)
==40693==
myarray[16] = 0
==40693==
==40693== HEAP SUMMARY:
==40693== in use at exit: 192 bytes in 9 blocks
==40693== total heap usage: 10 allocs, 1 frees, 1,216 bytes allocated
==40693==
==40693== LEAK SUMMARY:
==40693== definitely lost: 192 bytes in 9 blocks
==40693== indirectly lost: 0 bytes in 0 blocks
==40693== possibly lost: 0 bytes in 0 blocks
==40693== still reachable: 0 bytes in 0 blocks
==40693== suppressed: 0 bytes in 0 blocks
==40693== Rerun with --leak-check=full to see details of leaked memory
==40693==
==40693== For lists of detected and suppressed errors, rerun with: -s
==40693== ERROR SUMMARY: 2 errors from 2 contexts (suppressed: 0 from 0)

Visto? La compilazione non ha dato Warning (nonostante l'uso di Wall e pedantic) e anche l'esecuzione è andata apparentemente bene (che fortuna!). Poi Valgrind ha trovato tutti i problemi del programma, permettendoci di modificare a colpo sicuro il codice per evitarli. Ovviamente questo è un caso semplice e, come già detto sopra, possono esserci dei falsi positivi o informazioni un po criptiche (specialmente nei programmi multithread) che si devono interpretare. Comunque Valgrind è un aiuto preziosissimo e altamente raccomandabile anche (e soprattutto, direi) per programmi grandi e complessi, basta trovare la maniera di eseguirlo simulando carichi di lavoro reali.

Nell'esempio qui sopra l'interpretazione è abbastanza semplice: il numero ==40693== è il PID del processo eseguito, le prime linee seguenti sono, evidentemente, di presentazione, dopodiché vengono mostrati i veri e propri errori:

==40693== Invalid write of size 4
==40693== at 0x1091DA: main (test.c:16)
==40693== Address 0x4a9c080 is 0 bytes after a block of size 64 alloc d
==40693== at 0x4848899: malloc (in /usr/libexec/valgrind/vgpreload_memcheck-amd64-linux.so)
==40693== by 0x1091CD: main (test.c:13)

questo è, evidentemente, quello che nel codice è alla linea 14, come anticipato nel commento del codice:

// errore 1: scrivo una posizione non disponibile (write out-of-bound)
myarray[LEN_ARRAY] = 0;

Notare che viene fornito anche lo stack delle chiamate che finalizzano con la linea 16 di test.c. Dopodiché c'è il secondo errore:

==40693== Invalid read of size 4
==40693== at 0x1091E8: main (test.c:19)
==40693== Address 0x4a9c080 is 0 bytes after a block of size 64 alloc d
==40693== at 0x4848899: malloc (in /usr/libexec/valgrind/vgpreload_memcheck-amd64-linux.so)
==40693== by 0x1091CD: main (test.c:13)

questo è, evidentemente, quello che nel codice è alla linea 19, come anticipato nel commento del codice:

// errore 2: leggo una posizione non disponibile (read out-of-bound)
printf("myarray[%d] = %d\n", LEN_ARRAY, myarray[LEN_ARRAY]);

A questo punto, visto che Valgrind ha già mostrato gli errori effettivi, viene mostrato un riepilogo del'uso della memoria, che evidenzia il terzo errore, che non si riferisce alle istruzioni errate ma, bensì, alle istruzioni mancanti, le free(3):

==40693== HEAP SUMMARY:
==40693== in use at exit: 192 bytes in 9 blocks
==40693== total heap usage: 10 allocs, 1 frees, 1,216 bytes allocated
==40693==
==40693== LEAK SUMMARY:
==40693== definitely lost: 192 bytes in 9 blocks
==40693== indirectly lost: 0 bytes in 0 blocks
==40693== possibly lost: 0 bytes in 0 blocks
==40693== still reachable: 0 bytes in 0 blocks
==40693== suppressed: 0 bytes in 0 blocks

Come si nota, il numero di allocs e frees non corrisponde, il che indica che mancano 9 free(3) (1 per myarray e 8 per strings), e i byte persi, che sono 192, corrispondono esattamente ai dati allocati e non liberati.

Ok, per oggi può bastare. Vi sentite già esperti del Valgrind? Ecco, se lo provate su un programma grande, complesso e (magari) pieno di errori, il risultato potrebbe essere esteso e un po' preoccupante; in quel caso bisogna interpretare bene le informazioni e correggere uno per uno gli errori. Vi raccomando di farlo, vi eviterete molti mal di testa dovuti a problemi stranissimi segnalati dai clienti.

E poi mettiamo le cose in chiaro: l'obiettivo di un buon programmatore è produrre e distribuire Software bug-free... E si può! Si può'! Anzi in alcuni tipi di applicazioni, tipicamente quelle mission-critical e quelle business-critical, il Software distribuito deve essere sempre bug-free, ricordatelo! E Valgrind sarà un prezioso amico per raggiungere l'obiettivo, ve l'assicuro. Ovviamente dopo l'analisi statica vista all'inizio dell'articolo e la seguente analisi dinamica fatta con Valgrind mancherebbe una terza analisi, quella logica: cioè, se un programma è staticamente e dinamicamente perfetto ma non fa bene quello che dovrebbe fare non ci sono analizzatori che tengano: bisogna studiarlo e implementarlo bene! Tenetelo presente... ah ah ah.

Ciao, e al prossimo post!