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. Programmare bene in C è un'arte.
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.
lunedì 30 dicembre 2019
venerdì 20 dicembre 2019
sprintf? No, grazie! considerazioni sull'uso della sprintf in C
(...una premessa: questo post è un remake di un mio vecchio post. L'ho riadattato e "modernizzato" per pubblicarlo su quell'altro bel blog collettivo dove scrivo. Visto che le modifiche sono molte e (forse) interessanti, lo ripubblico anche qui. Questo potrebbe ripetersi in futuro...)
Premessa: la sprintf() se la usi bene e hai il 100% di controllo sul codice scritto la puoi usare senza grossi problemi ma, come dicono gli inglesi, è una funzione "error prone", e cioè induce facilmente a commettere errori, anche gravi. Il problema più grave ed evidente con la sprintf() si chiama buffer overflow, e non credo che sia necessario spenderci molte parole: se il buffer che passiamo come primo argomento non è correttamente dimensionato il disastro è dietro l'angolo.
La soluzione però è semplice, visto che, fortunatamente, ci viene in aiuto la snprintf(), che è della stessa famiglia ma molto più sicura. Vediamo i due prototipi a confronto:
E adesso siamo pronti per analizzare un semplicissimo esempio reale: scriviamo una funzione che ritorna una stringa formattata con data e ora (inclusi i microsecondi), e la implementiamo in due versioni, una buona che chiameremo getDateUsec(), e una cattiva che chiameremo badGetDateUsec(). Noterete che la versione cattiva passa solo l'array destinazione e internamente usa la sprintf(), mentre la versione buona passa l'array destinazione e il suo size, e internamente usa la snprintf(). Notare che è veramente una ottima abitudine quella di passare "destinazione + size" senza usare una allocazione interna alla funzione (con malloc()) che richiederebbe l'uso della corrispondente deallocazione (con free()) da parte del chiamante, come visto recentemente qui. Ed è anche una ottima abitudine evitare l'uso di una destinazione statica dentro la funzione, per i noti problemi con il multithreading (anche questo già visto qui). Ma bando alle ciance, vai col codice!
E già che siamo in argomento sprintf() un piccolo consiglio un po' OT: se dovete aggiungere sequenzialmente delle stringhe (in un loop, ad esempio) su una stringa base (per comporre un testo, ad esempio) non fate mai cosi:
Ciao e al prossimo post!
Peppe: Eva come fa uno a sapere se è innamorato di un'altra persona?Nel bel Perfetti Sconosciuti il personaggio Lele si caratterizza per le frasi a effetto come quella sopra. Con una leggera forzatura possiamo dire che quella frase e questo post si riferiscono allo stesso tema: l'abitudine. Lele pensa che lui e sua moglie si sono già detti tutto e, analogamente, i programmatori C usano la sprintf() solo perché è una pratica comune, una abitudine. Ecco, bisognerebbe fare un piccolo sforzo per risvegliare la passione originale (Lele, parla di più con tua moglie!) e cercare di evitare di trasformare una abitudine in una cattiva abitudine.
Eva: Eh, lo chiedi a me?
Peppe: Eh, sei tu che studi queste cose.
Bianca: Te lo dico io, allora se ci parli trenta minuti al giorno sei innamorato.
Peppe: E se ci parlo sessanta?
Carlotta: Eh allora vuol dire che sei molto innamorato.
Lele: Poi se non ce parli più, vordì che sei sposato!
![]() |
| ...poi, se non funziona, vordì che hai usato la sprintf... |
La soluzione però è semplice, visto che, fortunatamente, ci viene in aiuto la snprintf(), che è della stessa famiglia ma molto più sicura. Vediamo i due prototipi a confronto:
int sprintf(char *str, const char *format, ...); int snprintf(char *str, size_t size, const char *format, ...);Come si può ben vedere, la snprintf() ci obbliga a mettere il size del buffer come secondo argomento, per cui è facilissimo abituarsi a scrivere in un modo error-free come questo:
char buffer[32]; snprintf(buffer, sizeof(buffer), "Hello world!");E, se al posto di "Hello world!" avessimo scritto una stringa di più di 32 chars, nessun problema: la snprintf() la tronca opportunamente e siamo salvi. Fantastico.
E adesso siamo pronti per analizzare un semplicissimo esempio reale: scriviamo una funzione che ritorna una stringa formattata con data e ora (inclusi i microsecondi), e la implementiamo in due versioni, una buona che chiameremo getDateUsec(), e una cattiva che chiameremo badGetDateUsec(). Noterete che la versione cattiva passa solo l'array destinazione e internamente usa la sprintf(), mentre la versione buona passa l'array destinazione e il suo size, e internamente usa la snprintf(). Notare che è veramente una ottima abitudine quella di passare "destinazione + size" senza usare una allocazione interna alla funzione (con malloc()) che richiederebbe l'uso della corrispondente deallocazione (con free()) da parte del chiamante, come visto recentemente qui. Ed è anche una ottima abitudine evitare l'uso di una destinazione statica dentro la funzione, per i noti problemi con il multithreading (anche questo già visto qui). Ma bando alle ciance, vai col codice!
#include <stdio.h> #include <stdlib.h> #include <sys/time.h> #include <time.h> // prototipi locali char *getDateUsec(char *dest, size_t size); char *badGetDateUsec(char *dest); // funzione main int main(int argc, char* argv[]) { // chiama getDateUsec (o badGetDateUsec) e scrive il risultato char dest[12]; printf("date con usec: %s\n", getDateUsec(dest, sizeof(dest))); //printf("date con usec: %s\n", badGetDateUsec(dest)); return EXIT_SUCCESS; } // getDateUsec() - Genera una stringa con data e ora (usa i microsecondi) char *getDateUsec(char *dest, size_t size) { // get time: gettimeofday()+localtime() invece di time()+localtime() per ottenere i usec struct timeval tv; gettimeofday(&tv, NULL); struct tm *tmp = localtime(&tv.tv_sec); // format stringa destinazione dest (deve essere allocata dal chiamante) e aggiunge i usec char fmt[128]; strftime(fmt, sizeof(fmt), "%Y-%m-%d %H:%M:%S.%%06u", tmp); snprintf(dest, size, fmt, tv.tv_usec); // return stringa destinazione dest return dest; } // badGetDateUsec() - Genera una stringa con data e ora (usa i microsecondi) (versione bad) char *badGetDateUsec(char *dest) { // get time: gettimeofday()+localtime() invece di time()+localtime() per ottenere i usec struct timeval tv; gettimeofday(&tv, NULL); struct tm *tmp = localtime(&tv.tv_sec); // format stringa destinazione dest (deve essere allocata dal chiamante) e aggiunge i usec char fmt[128]; strftime(fmt, sizeof(fmt), "%Y-%m-%d %H:%M:%S.%%06u", tmp); sprintf(dest, fmt, tv.tv_usec); // return stringa destinazione dest return dest; }Provate a compilare l'esempio dove, volutamente, ho sottodimensionato il buffer di destinazione: commentando la badGetDateUsec() e usando la getDateUsec(), funziona perfettamente, troncando l'output a 12 chars. Se, invece, si commenta la getDateUsec() e si usa la badGetDateUsec() il programma si schianta durante l'esecuzione. Provare per credere!
E già che siamo in argomento sprintf() un piccolo consiglio un po' OT: se dovete aggiungere sequenzialmente delle stringhe (in un loop, ad esempio) su una stringa base (per comporre un testo, ad esempio) non fate mai cosi:
char buf[256] = ""; for (int i = 0; i < 10; i++) sprintf(buf, "%s aggiunto alla stringa %d\n", buf, i);il metodo qui sopra sembra funzionare, ma, in realtà, funziona quando c'ha voglia lui. Fate invece così:
char buf[256] = ""; for (int i = 0; i < 10; i++) { char tmpbuf[256]; sprintf(tmpbuf, "%s aggiunto alla stringa %d\n", buf, i); sprintf(buf, "%s", tmpbuf); }E se non ci credete provate a passare il codice cattivo con un lint tipo cppchek, e il risultato sarà questo:
(error) Undefined behavior: Variable 'buf' is used as parameter and destination in s[n]printf().Quanto sopra è, del resto, ben specificato nel manuale della sprintf():
C99 and POSIX.1-2001 specify that the results are undefined if a call to sprintf(), snprintf(), vsprintf(), or vsnprintf() would cause copy‐ ing to take place between objects that overlap (e.g., if the target string array and one of the supplied input arguments refer to the same buffer).E, ovviamente, anche in quest’ultimo esempio, fatto per semplicità con la sprintf() ,sarebbe stato raccomandabile usare la snprintf(). E per oggi è tutto, sono già entrato in fase pre-natalizia, quindi cominciamo a rilassarci...
Ciao e al prossimo post!
martedì 19 novembre 2019
Amore e malloc considerazioni sull'uso della malloc() in C
(...una premessa: questo post è un remake di un mio vecchio post. L'ho riadattato e "modernizzato" per pubblicarlo su quell'altro bel blog collettivo dove scrivo. Visto che le modifiche sono molte e (forse) interessanti, lo ripubblico anche qui. Questo potrebbe ripetersi in futuro...)
Allora: se facciamo una rapida ricerca con Google sulla famigerata malloc()
e sul suo uso (provare con "why use malloc", per esempio), noteremo una
notevole quantità di interrogativi sull'argomento che vanno dai
semplici dubbi ("ma come si usa ?") ai dubbi esistenziali ("perchè si usa ?").
Beh, ci troviamo su un argomento pseudo-filosofico dovuto al fatto che,
effettivamente, è possibile programmare a lungo in C senza mai usare la
malloc()...
Cominciamo con una "premessa metodologica" per definire di cosa non stiamo parlando: chiunque (incluso il sottoscritto) si trovi con la necessità di scrivere (molto) rapidamente un piccolo programma, per testare rapidamente qualcosa - chissà per una urgenza improvvisa durante qualche manutenzione software critica e urgentissima (da terminare per ieri) - come lo scrive ? Ovviamente usando a man bassa variabili automatiche, array sovradimensionati ("memoria ce n'è tanta"), magari qualche variabile statica e (orrore, orrore) anche variabili globali. Ecco, insomma, uno di quei programmi che già mentre lo scrivi lo battezzi "temp_qualcosa.c" perché sai che dovrai rifarlo da capo appena possibile o dovrai cancellarlo per la vergogna (mica che qualcuno lo legge per sbaglio).
Ma oggi non parleremo di casi limite come quello della premessa qui sopra, parleremo di codice ben scritto, quello in cui abbiamo il tempo e i mezzi per decidere se usare o meno la malloc(). Procediamo: tutti sappiamo che il C ha una potente e flessibile gestione della memoria (è un linguaggio con i puntatori !), e scrivere in C facendo finta di non saperlo è un errore. Attenzione però: questo non significa che bisogna per forza usare la malloc() ("se no non sei un buon programmatore"), anzi è vero il contrario. All'interno di una funzione le variabili automatiche saranno sempre (e giustamente) la maggioranza, anche perché (quando è possibile e corretto) usare lo stack invece dello heap migliora le prestazioni del programma e, tra l'altro, lo rende più semplice da scrivere, leggere e manutenere. La allocazione dinamica della memoria è una operazione dispendiosa per il sistema operativo, aggiunge (evidentemente) complicazione al codice e aumenta la possibilità di errori (alzi la mano chi non si è mai dimenticato di usare la corrispondente free() di una malloc()...).
(...apro una parentesi: si, lo so, lo so, parlare solo di stack e heap è un po' generico, bisognerebbe parlare anche di Free Store, Global/Static, ecc. E poi, il termine stack non è mai esplicitamente usato nello Standard del C... ma sappiamo di cosa stiamo parlando, no? Non facciamo troppo i pedanti, come pedante basto e avanzo io...)
Ma allora quando e perché usare la malloc()? Scusate la risposta un po' lapalissiana, ma io direi:
1) quando è indispensabile
2) quando è meglio
E allora vediamo il primo punto di La Palice:
Quando è indispensabile? La allocazione dinamica è indispensabile in almeno due casi: il primo è quello delle funzioni che ritornano un indirizzo. Vediamo un semplice esempio di funzione che duplica una stringa (ce n'è uno quasi identico sul K&R):
Il secondo caso con scelta obbligata è quello delle linked list (o, più in generale, queues, stacks e oggetti similari). Vediamo un esempio molto semplificato (mentre un esempio completo compilabile e testabile lo potete trovare qui):
E ora passiamo al secondo punto di La Palice:
Quando è meglio? Sicuramente tutte le volte che si devono maneggiare dei dati (tipicamente array di tipi semplici, array di strutture, testi, ecc.), di cui non si conosce a compile-time la dimensione: se non usassimo la malloc() bisognerebbe allocare dati sovradimensionati (per evitare che manchi spazio a run-time). E quest'ultimo dettaglio ci indica un altro punto: se dobbiamo maneggiare grosse quantità di dati non possiamo confidare troppo nello stack, il cui spazio non è infinito, anche se lavoriamo con moderni OS su macchine piene di memoria (e se programmiamo applicazioni embedded con forti limitazioni Hardware... ancora peggio).
Un esempio classico? Scriviamo una funzione che estrae il testo di un file e lo ritorna al chiamante:
E a questo punto ci vuole una bella tabella riassuntiva, per fornire una visione schematica e immediata degli argomenti trattati. Vai con la tabella!
Beh, con questo è tutto, e ricordate: per ogni malloc() c'è una free(): se i conti non vi tornano cominciate a preoccuparvi...
Ciao e al prossimo post!
Boris: Sonja, e se Dio non esistesse?I film del Maestro Woody Allen sono sempre infarciti di disquisizioni filosofiche sul senso della vita e altre cosucce del genere. Il bel Amore e Guerra, non fa eccezione, e ci propone un quesito pesante che possiamo (ehm...) equiparare a uno dei grandi dilemmi che tormentano i programmatori C: ebbene si, oggi parleremo di allocazione dinamica della memoria: come, quando e perché usarla (uff, argomento pesantissimo...).
Sonja: Boris Dimitrovic, stai scherzando?
Boris: E se fossimo solo un branco di gente assurda che corre intorno senza nesso o ragione?
Sonja: Ma se non esiste Dio la vita non avrebbe alcun significato, perché dovremmo continuare a vivere? Perché allora non suicidarsi?
Boris: Be', non facciamo gli isterici, potrei sbagliare. Io oggi mi uccido e domani "lui" concede un'intervista!
![]() |
| ...e se la malloc() non esistesse?... |
Cominciamo con una "premessa metodologica" per definire di cosa non stiamo parlando: chiunque (incluso il sottoscritto) si trovi con la necessità di scrivere (molto) rapidamente un piccolo programma, per testare rapidamente qualcosa - chissà per una urgenza improvvisa durante qualche manutenzione software critica e urgentissima (da terminare per ieri) - come lo scrive ? Ovviamente usando a man bassa variabili automatiche, array sovradimensionati ("memoria ce n'è tanta"), magari qualche variabile statica e (orrore, orrore) anche variabili globali. Ecco, insomma, uno di quei programmi che già mentre lo scrivi lo battezzi "temp_qualcosa.c" perché sai che dovrai rifarlo da capo appena possibile o dovrai cancellarlo per la vergogna (mica che qualcuno lo legge per sbaglio).
Ma oggi non parleremo di casi limite come quello della premessa qui sopra, parleremo di codice ben scritto, quello in cui abbiamo il tempo e i mezzi per decidere se usare o meno la malloc(). Procediamo: tutti sappiamo che il C ha una potente e flessibile gestione della memoria (è un linguaggio con i puntatori !), e scrivere in C facendo finta di non saperlo è un errore. Attenzione però: questo non significa che bisogna per forza usare la malloc() ("se no non sei un buon programmatore"), anzi è vero il contrario. All'interno di una funzione le variabili automatiche saranno sempre (e giustamente) la maggioranza, anche perché (quando è possibile e corretto) usare lo stack invece dello heap migliora le prestazioni del programma e, tra l'altro, lo rende più semplice da scrivere, leggere e manutenere. La allocazione dinamica della memoria è una operazione dispendiosa per il sistema operativo, aggiunge (evidentemente) complicazione al codice e aumenta la possibilità di errori (alzi la mano chi non si è mai dimenticato di usare la corrispondente free() di una malloc()...).
(...apro una parentesi: si, lo so, lo so, parlare solo di stack e heap è un po' generico, bisognerebbe parlare anche di Free Store, Global/Static, ecc. E poi, il termine stack non è mai esplicitamente usato nello Standard del C... ma sappiamo di cosa stiamo parlando, no? Non facciamo troppo i pedanti, come pedante basto e avanzo io...)
Ma allora quando e perché usare la malloc()? Scusate la risposta un po' lapalissiana, ma io direi:
1) quando è indispensabile
2) quando è meglio
E allora vediamo il primo punto di La Palice:
Quando è indispensabile? La allocazione dinamica è indispensabile in almeno due casi: il primo è quello delle funzioni che ritornano un indirizzo. Vediamo un semplice esempio di funzione che duplica una stringa (ce n'è uno quasi identico sul K&R):
// myStrdup() - crea un duplicato di una stringa char *myStrdup( const char *src) { // crea un duplicato di src (con spazio per il '\0') char *dest = malloc(strlen(src) + 1); if (dest != NULL) strcpy(dest, src); // ritorna il duplicato return dest; }Semplice, no? Inutile dire (ma lo dico lo stesso) che, in una funzione come questa, si deve per forza usare malloc() e non si può optare per usare l'indirizzo di un array automatico allocato sullo stack, che, proprio perché è sullo stack, si perde dopo il return (errore frequentissimo questo, ahimè).
Il secondo caso con scelta obbligata è quello delle linked list (o, più in generale, queues, stacks e oggetti similari). Vediamo un esempio molto semplificato (mentre un esempio completo compilabile e testabile lo potete trovare qui):
// nodo di una linked list singola con campo dati typedef struct snode { int data; struct snode *next; } node_t;
// 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; }E anche in questo caso risulta evidente che scrivere una linked list senza usare malloc() sarebbe veramente problematico.
E ora passiamo al secondo punto di La Palice:
Quando è meglio? Sicuramente tutte le volte che si devono maneggiare dei dati (tipicamente array di tipi semplici, array di strutture, testi, ecc.), di cui non si conosce a compile-time la dimensione: se non usassimo la malloc() bisognerebbe allocare dati sovradimensionati (per evitare che manchi spazio a run-time). E quest'ultimo dettaglio ci indica un altro punto: se dobbiamo maneggiare grosse quantità di dati non possiamo confidare troppo nello stack, il cui spazio non è infinito, anche se lavoriamo con moderni OS su macchine piene di memoria (e se programmiamo applicazioni embedded con forti limitazioni Hardware... ancora peggio).
Un esempio classico? Scriviamo una funzione che estrae il testo di un file e lo ritorna al chiamante:
// readFile() - legge un file di testo e lo ritorna in un buffer char *readFile( const char *file_name) // il nome del file { // apre il file int fd; if ((fd = open(file_name, O_RDONLY)) < 0) return NULL; // ricava il size struct stat stbuf; if (fstat(fd, &stbuf) < 0) return NULL; // alloca il buffer char *buffer; off_t file_size = stbuf.st_size; if ((buffer = (char*)malloc(file_size + 1)) == NULL) return NULL; // legge il file size_t n_read; if ((n_read = read(fd, buffer, file_size)) <= 0) return NULL; // aggiunge il terminatore e ritorna il buffer buffer[n_read] = 0; return buffer; // questo buffer deve liberarlo il chiamante con free() }Nel semplice esempio qui sopra il chiamante della funzione readFile() deve solo fornire il nome del file da leggere (e di cui a compile-time non si conosce né il nome né il size). La funzione alloca un buffer (del size necessario a contenere il testo) e lo ritorna riempito al chiamante che deve solo preoccuparsi di cancellarlo (con free()) quando non ne ha più bisogno (è un codice semplificato, eh!).
E a questo punto ci vuole una bella tabella riassuntiva, per fornire una visione schematica e immediata degli argomenti trattati. Vai con la tabella!
Elementi da considerare per decidere se usare l'allocazione dinamica Scope e Lifetime (visibilità e durata di una variabile in una funzione) stack valide solo dentro la funzione, si perdono quando la funzione esce (variabili automatiche). heap valide dalla creazione alla cancellazione, non si perdono quando la funzione esce (variabili "mallocate"). Spazio stack lo spazio è limitato: da usare solo per oggetti piccoli (notare che anche una funzione che usa piccole variabili automatiche potrebbe causare stack-overflow se chiamata ricorsivamente). heap lo spazio è molto grande: da usare per oggetti grandi che, altrimenti, potrebbero causare stack-overflow. Dimensionamento stack è preferibile per variabili con dimensione conosciuta a compile-time. heap da usare per variabili con dimensione che si conoscerà a run-time. Velocità stack è molto veloce, con tempo (quasi) costante e non frammenta la memoria. heap è lento, con tempo non predicibile e frammenta la memoria (quindi bisogna prestare attenzione a dove e come usare la malloc(), per evitare di appesantire il programma). Affidabilità stack un eventuale stack-overflow è molto difficile da valutare e debuggare. heap è facile intercettare/interpretare gli errori e prevenirli. Casi Reali stack uso raccomandato per tutti i casi in cui scope e lifetime estesi non sono una necessità. heap uso indispensabile per scrivere funzioni che ritornano un indirizzo e per linked-lists, stacks e queues. Uso raccomandato per oggetti molto grandi.Con quanto detto fin'ora spero, almeno, di aver contribuito a fare un po' di chiarezza su quest'argomento controverso. Ah, nel post ho citato sempre, per semplicità, la malloc(), ma, come ben tutti sanno, per l'allocazione dinamica della memoria c'è una vera famiglia di funzioni (malloc(), calloc(), realloc() e free()) che permettono una notevole flessibilità e varietà di soluzioni.
Beh, con questo è tutto, e ricordate: per ogni malloc() c'è una free(): se i conti non vi tornano cominciate a preoccuparvi...
Ciao e al prossimo post!
Iscriviti a:
Post (Atom)


