Menu

printf in C: stringhe di formato, specificatori e stampa dei float

Come funziona davvero printf in C: il modello della stringa di formato, gli specificatori che usi ogni giorno, larghezza e precisione per un output allineato, stampare i float in modo sensato, il valore di ritorno e perché printf(user_input) è una falla di sicurezza.

Questa pagina include editor eseguibili: modifica, esegui e vedi subito l'output.

printf è la prima funzione che ogni programmatore C impara e quella di cui quasi nessuno legge mai tutto il manuale. Vale dieci minuti, perché la sua stringa di formato è un piccolo linguaggio a sé, e perché sbagliare uno specificatore non è un refuso: è comportamento indefinito.

Si trova in <stdio.h>, quindi ogni programma che la usa inizia con #include <stdio.h>.

Il modello della stringa di formato

printf percorre la stringa di formato un carattere alla volta. Tutto ciò che non è un % viene stampato esattamente come è scritto. Un % inizia una specifica di conversione, che consuma l'argomento successivo e lo stampa nella forma richiesta.

Gli argomenti vengono abbinati agli specificatori rigorosamente per posizione: il primo % prende count, il secondo prende name e così via. printf non ha modo di controllare che tu abbia passato ciò che hai promesso, perché non vede i tipi, quindi la stringa di formato è un contratto che devi rispettare tu. Ecco perché conviene attivare -Wall: GCC legge la stringa di formato letterale durante la compilazione e avvisa quando un argomento non corrisponde.

Nessuno impone un a capo. printf("Hello") lascia il cursore dopo la o, ed è per questo che quasi ogni stringa di formato finisce con \n.

Gli specificatori che usi ogni giorno

Cinque coprono la maggior parte del codice:

SpecificatoreStampaEsempio di chiamataOutput
%dun int con segnoprintf("%d", -42)-42
%fun doubleprintf("%f", 2.5)2.500000
%suna stringa (char *)printf("%s", "hi")hi
%cun singolo carattereprintf("%c", 'A')A
%pl'indirizzo di un puntatoreprintf("%p", (void*)&x)0x7ffd...

In questo frammento si nascondono due regole. Un % letterale si scrive %%: un % isolato alla fine di una stringa di formato è comportamento indefinito. E %p richiede un void *, quindi fai il cast del puntatore; passare direttamente un int * è tecnicamente indefinito, anche se in pratica stampa senza problemi.

La tabella completa, con ogni specificatore, ogni modificatore di lunghezza e la colonna di scanf, si trova nella pagina sugli specificatori di formato. Questa pagina resta su quelli che usi davvero.

Larghezza, precisione e allineamento

Tra il % e la lettera puoi mettere dei flag, una larghezza e una precisione. È così che trasformi un ciclo in una tabella.

La larghezza è un minimo, mai un massimo: %3d con il valore 123456 stampa tutte e sei le cifre. La precisione significa cose diverse a seconda del tipo: cifre dopo il punto decimale per %f, numero massimo di caratteri per %s, numero minimo di cifre per %d.

Entrambe si possono fornire a runtime con un *, che legge un argomento int:

Cambiando width si ridisegna l'intera tabella. È il modo standard per stampare colonne allineate in C, senza bisogno di funzioni di supporto per il riempimento delle stringhe.

Stampare i float in modo sensato

%f senza precisione stampa sempre sei cifre dopo il punto, che raramente è quello che si vuole:

Tre scelte, e una regola pratica per ciascuna:

  • %.Nf per soldi, misure e tutto ciò che ha un numero naturale di decimali. %.2f è quello che ti serve per le valute.
  • %e per la notazione scientifica, sempre nella forma d.dddddde±dd.
  • %g quando non conosci in anticipo l'ordine di grandezza. Sceglie tra %f e %e quello più corto ed elimina gli zeri finali: il default più sensato per registrare un valore che non hai ancora visto.

Una trappola da dire chiaramente: in printf non esiste %lf. Gli argomenti float vengono promossi a double prima che printf li veda, quindi %f gestisce entrambi, e %lf ha senso solo in scanf, dove conta la dimensione della destinazione. (C99 accetta %lf in printf come sinonimo, ma l'abitudine di scriverlo porta a scrivere %f in scanf, e lì si rompe davvero.)

Interi di altre dimensioni

%d è per gli int. I tipi più grandi o senza segno richiedono un modificatore di lunghezza, e sbagliarlo è comportamento indefinito, non un errore di arrotondamento:

%zu per size_t è quello che si sbaglia più spesso: sizeof restituisce un size_t, non un int, quindi printf("%d", sizeof x) è un bug che per caso funziona sui sistemi a 32 bit e si comporta male su quelli a 64 bit.

Il valore di ritorno

printf restituisce il numero di caratteri che ha scritto, oppure un numero negativo in caso di errore. Di solito viene ignorato e quasi sempre va bene così, ma è davvero utile quando devi sapere quanto era largo l'output:

Le funzioni collegate sprintf e snprintf scrivono in un buffer invece che sullo schermo e restituiscono la lunghezza prodotta. Preferisci sempre snprintf, che riceve la dimensione del buffer e non può andare in overflow:

snprintf restituisce la lunghezza che sarebbe servita per la stringa completa, quindi un valore di ritorno uguale o superiore alla dimensione del buffer ti dice che l'output è stato troncato.

La vulnerabilità delle stringhe di formato

È l'unico problema di sicurezza di printf, ed è grave. Non passare mai come stringa di formato dati che non hai scritto tu:

/* PERICOLOSO: non farlo mai */
printf(user_input);

/* Corretto */
printf("%s", user_input);

Se user_input contiene %s, printf legge un argomento mai passato e segue qualunque spazzatura a forma di puntatore trovi, di solito andando in crash. %x ripetuto riversa sullo schermo il contenuto dello stack, che può includere password o chiavi. E %n, che scrive il conteggio dei caratteri in un argomento, storicamente ha trasformato tutto questo in corruzione arbitraria della memoria ed esecuzione di codice remoto.

La regola è assoluta e non costa nulla: la stringa di formato è sempre un letterale scritto da te. Tutto ciò che è variabile entra come argomento. L'opzione -Wformat-security di GCC segnala l'errore ed è inclusa in -Wall -Wextra.

Buffering: perché a volte l'output arriva in ritardo

printf non scrive subito sul terminale. L'output passa da un buffer, che viene svuotato quando si riempie, quando compare un a capo su un terminale o quando il programma termina normalmente. Due conseguenze:

  • Un programma che va in crash può perdere output che "era già stato stampato". Se fai debug con printf e l'ultimo messaggio non compare mai, il crash è avvenuto dopo quella riga, non prima. Aggiungi fflush(stdout) oppure manda l'output di debug su stderr, che non ha buffer.
  • fprintf(stderr, ...) è il modo giusto per stampare gli errori. Non ha buffer e va su uno stream separato, che gli utenti possono reindirizzare indipendentemente dall'output normale.

Domande frequenti

Come funziona printf in C?

printf riceve una stringa di formato e la scorre carattere per carattere. I caratteri normali vengono stampati così come sono; ogni % avvia una specifica di conversione che consuma uno degli argomenti successivi e lo stampa nella forma richiesta. printf("%d mele\n", 5) stampa 5 mele e un a capo.

Come si stampa un float con 2 decimali in C?

Usa una precisione: printf("%.2f\n", 3.14159) stampa 3.14. Il numero dopo il punto indica quante cifre seguono il punto decimale. %f senza precisione stampa sempre 6 cifre, ed è per questo che i float non formattati sembrano così rumorosi.

Perché printf stampa valori spazzatura o va in crash?

Quasi sempre per una discrepanza tra lo specificatore e l'argomento. printf("%d", 3.5) o printf("%s", 42) sono comportamento indefinito: printf non può vedere i tipi reali e si fida della stringa di formato. Compila con -Wall, così GCC confronta il formato con gli argomenti.

Perché printf(user_input) è pericoloso?

Se il testo dell'utente contiene sequenze %, printf le esegue e legge argomenti che non sono mai stati passati, facendo trapelare memoria dello stack e, con %n, arrivando perfino a scrivere in memoria. È la classica vulnerabilità delle stringhe di formato. Scrivi sempre printf("%s", user_input).

Illustrazione dei linguaggi di programmazione di Coddy

Impara a programmare con Coddy

INIZIA