Introduzione alla funzione printf in linguaggio C

La funzione printf in linguaggio C è una delle funzioni più utilizzate per la stampa di output sullo schermo. Fa parte della libreria standard di input/output (stdio.h) e consente di formattare e visualizzare vari tipi di dati in modo flessibile.

Per questo motivo, è fondamentale comprendere dall'inizio come utilizzare correttamente printf per sfruttarne appieno le potenzialità.

Concetti Chiave
  • La funzione printf è una delle funzioni più comuni in linguaggio C per la stampa di output formattato sullo schermo.
  • La sintassi generale di printf prevede una stringa di formato seguita da uno o più argomenti da stampare.
  • Gli specificatori di formato, che iniziano con il simbolo di percentuale (%), indicano dove inserire i valori da stampare e come formattarli.
  • È importante che il numero di specificatori di formato corrisponda al numero di argomenti forniti, altrimenti si potrebbero verificare comportamenti imprevisti.
  • Le sequenze di escape, come \n per andare a capo, consentono di inserire caratteri speciali all'interno delle stringhe di formato.

Funzionamento di base della funzione printf

La funzione printf è progettata per mostrare il contenuto di una stringa, che prende il nome di stringa di formato, con dei valori che possono essere posizionati all'interno di essa in alcuni punti specifici.

Una stringa è una sequenza di caratteri racchiusa tra virgolette doppie ("); torneremo sulle stringhe nelle prossime lezioni. Per ora, ci basti sapere che una stringa di formato può contenere del testo normale e dei segnaposto (o specificatori di formato) che indicano dove inserire i valori da stampare.

Quando invochiamo printf (ossia quando chiamiamo la funzione), dobbiamo specificare almeno un argomento: la stringa di formato. Eventualmente, possiamo fornire ulteriori argomenti che corrispondono ai segnaposto presenti nella stringa di formato.

La sintassi generale della funzione printf è la seguente:

printf("stringa di formato", argomento1, argomento2, ...);

Gli argomenti successivi alla stringa di formato possono essere valori costanti, variabili o espressioni più complesse.

In generale non c'è un limite al numero di valori che può essere stampato con una singola chiamata a printf, purché il numero di segnaposto nella stringa di formato corrisponda al numero di argomenti forniti.

Come menzionato, la stringa di formato può contenere sia testo normale, ossia normali caratteri, sia i cosiddetti specificatori di formato. Uno specificatore di formato è una sequenza di caratteri che inizia con il simbolo di percentuale (%) e indica sia la posizione in cui inserire un valore, sia il tipo di dato che si desidera stampare.

In particolare, le informazioni che seguono il simbolo di percentuale specificano come un valore deve essere convertito dalla sua rappresentazione binaria interna a una rappresentazione testuale leggibile dall'utente.

Ad esempio, lo specificatore di formato %d indica che il valore da stampare è un intero decimale con segno, mentre %f indica che il valore è un numero in virgola mobile (floating-point).

I caratteri normali presenti nella stringa di formato vengono stampati così come sono, senza alcuna modifica. Viceversa, ogniqualvolta viene incontrato uno specificatore di formato, printf preleva il corrispondente argomento dalla lista degli argomenti forniti e lo converte in una rappresentazione testuale secondo le regole definite dallo specificatore.

Consideriamo un esempio:

int a, b;
double x, y;

a = 10;
b = 20;
x = 3.14;
y = 2.71;

printf("Valori interi: a = %d, b = %d\n", a, b);
printf("Valori in virgola mobile: x = %lf, y = %lf\n", x, y);

In questo esempio, la prima chiamata a printf stampa i valori delle variabili a e b utilizzando lo specificatore di formato %d per gli interi. La seconda chiamata stampa i valori delle variabili x e y utilizzando lo specificatore di formato %lf per i numeri in virgola mobile di tipo double.

Il risultato dell'esecuzione di questo codice sarà:

Valori interi: a = 10, b = 20
Valori in virgola mobile: x = 3.140000, y = 2.710000

Come si può notare, i caratteri normali nella stringa di formato sono stati stampati così come sono. Gli specificatori di formato %d e %lf sono stati sostituiti dai valori delle variabili corrispondenti, convertiti in una rappresentazione testuale appropriata.

Un'altra osservazione riguarda il fatto che, nel sostituire gli specificatori di formato con i valori corrispondenti, printf segue l'ordine in cui gli argomenti sono stati forniti. Nel nostro esempio, il primo %d corrisponde al primo argomento a, il secondo %d al secondo argomento b, e così via.

Ricapitolando:

Definizione

Funzione printf

La funzione printf consente di stampare testo formattato sullo schermo. La sua sintassi generale è:

printf("stringa di formato", argomento1, argomento2, ...);

La stringa di formato può contenere testo normale e specificatori di formato, che iniziano con il simbolo di percentuale (%) e indicano dove inserire i valori da stampare e come formattarli.

Gli argomenti successivi alla stringa di formato corrispondono ai segnaposto presenti nella stringa e vengono convertiti in rappresentazioni testuali secondo le regole definite dagli specificatori di formato.

Nell'utilizzare la funzione printf, però, bisogna fare attenzione a un aspetto fondamentale: il numero di specificatori di formato presenti nella stringa deve corrispondere esattamente al numero di argomenti forniti dopo la stringa stessa.

Nota

Attenzione al numero di argomenti della funzione printf

In generale, secondo lo standard del linguaggio C, non è necessario che il compilatore controlli che il numero di specificatori di formato nella stringa corrisponda al numero di argomenti forniti.

Ad esempio, il seguente codice è errato:

/* ERRORE: numero di argomenti insufficiente */
printf("Valori: a = %d, b = %d\n", a);

In questo caso, la stringa di formato contiene due specificatori di formato (%d), ma viene fornito solo un argomento (a). Tuttavia, il compilatore potrebbe non segnalare alcun errore ma, anzi, compilare il programma senza problemi. Nonostante ciò, l'esecuzione del programma potrebbe portare a comportamenti imprevisti, come la stampa di valori errati o addirittura il crash del programma.

Lo stesso vale per il caso opposto, in cui vengono forniti più argomenti di quanti ne siano necessari:

/* ERRORE: numero di argomenti in eccesso */
printf("Valore: a = %d\n", a, b);

In questo caso, la stringa di formato contiene un solo specificatore di formato (%d), ma vengono forniti due argomenti (a e b). Anche in questo caso, il compilatore potrebbe non segnalare alcun errore, ma l'esecuzione del programma potrebbe portare a comportamenti imprevisti.

In generale, è responsabilità del programmatore assicurarsi che il numero di specificatori di formato corrisponda al numero di argomenti forniti. In caso contrario, si rischia di incorrere in errori difficili da individuare e correggere.

Un altro aspetto importante da considerare riguarda il tipo di dato associato a ciascun specificatore di formato. Ogni specificatore di formato è progettato per gestire un tipo di dato specifico, e fornire un argomento di tipo errato può portare a comportamenti imprevisti.

Nota

Attenzione al tipo di dato degli argomenti della funzione printf

Così come lo standard del linguaggio C non richiede al compilatore di controllare il numero di argomenti forniti a printf, non richiede nemmeno di verificare che il tipo di dato di ciascun argomento corrisponda al tipo previsto dallo specificatore di formato.

Se lo sviluppatore fornisce un argomento di tipo errato, la funzione printf potrebbe, nel migliore dei casi, stampare valori senza senso, oppure, nel peggiore dei casi, causare il crash del programma.

Consideriamo il seguente esempio:

int a = 10;
double b = 3.14;

// ERRORE: tipi di dato errati
printf("%lf %d \n", a, b);

In questo caso, il primo specificatore di formato %lf si aspetta un argomento di tipo double, ma viene fornito un argomento di tipo int (a). Allo stesso modo, il secondo specificatore di formato %d si aspetta un argomento di tipo int, ma viene fornito un argomento di tipo double (b).

Dal momento che la funzione printf segue pedissequamente le specifiche fornite nella stringa di formato, essa stamperà un double seguito da un int, ma i valori stampati non avranno alcun senso, poiché i tipi di dato non corrispondono.

Per evitare questi problemi, è fondamentale che lo sviluppatore si assicuri che il tipo di dato di ciascun argomento corrisponda al tipo previsto dallo specificatore di formato corrispondente.

Introduzione agli Specificatori di formato

Gli specificatori di formato forniscono una grande capacità di controllo sull'output generato dalla funzione printf. Ma, spesso, possono risultare criptici e complessi da leggere, specialmente per gli sviluppatori neofiti.

Infatti, a questo punto della guida è troppo prematuro fornire tutte i dettagli su di essi e sulle loro funzionalità avanzate. Rimandiamo questo argomento alle lezioni successive, dove verranno trattati in modo più approfondito.

Per il momento, ci concentreremo sui loro aspetti principali.

In generale, la forma più comune e semplice di uno specificatore di formato è la seguente:

%-m.pX

Questa sintassi è composta da alcune parti obbligatorie:

  • Il simbolo di percentuale (%), che indica l'inizio di uno specificatore di formato. Questo simbolo è obbligatorio e deve essere presente in ogni specificatore di formato.
  • Una combinazione di lettere, X, anch'essa obbligatoria, che indica il tipo di dato da stampare. Ad esempio, d per gli interi decimali, lf per i numeri in virgola mobile di tipo double, c per i caratteri, e così via.

E da alcune parti opzionali:

  • Un segno meno (-), che indica l'allineamento a sinistra del valore stampato. Se non è presente, il valore viene allineato a destra per default.
  • Un numero intero positivo, m, che specifica la larghezza minima del campo in cui il valore verrà stampato. Se il valore è più corto di questa larghezza, verrà riempito con spazi vuoti.
  • Un punto (.) seguito da un numero intero positivo, p, che specifica la precisione del valore stampato. Per i numeri in virgola mobile, questo indica il numero di cifre da stampare dopo il punto decimale. Se p non è specificato anche il punto deve essere omesso.

Larghezza minima del campo

La larghezza minima del campo (m) specifica il numero minimo di caratteri che devono essere utilizzati per stampare il valore. Se il valore da stampare è più corto di questa larghezza, il valore verrà stampato allineandolo a destra e verranno aggiunti degli spazi vuoti a sinistra per raggiungere la larghezza minima.

Per esempio, supponiamo di voler stampare il numero intero 123 con una larghezza minima del campo di 5:

int num = 123;
printf("Numero: %5d\n", num);

L'output sarà:

Numero:   123

In questo caso, il numero 123 viene stampato con due spazi vuoti a sinistra per raggiungere la larghezza minima di 5 caratteri.

Viceversa, se il valore da stampare è più lungo della larghezza minima specificata, il valore verrà stampato per intero senza alcuna modifica.

Ad esempio, se volessimo stampare il numero intero 12345 con una larghezza minima del campo di 3:

int num = 12345;
printf("Numero: %3d\n", num);

L'output sarà:

Numero: 12345

Quindi, in questo caso, la larghezza minima del campo è stata ignorata perché il valore da stampare è più lungo di 3 caratteri. In questo modo, printf garantisce che il valore venga sempre stampato per intero, indipendentemente dalla larghezza minima specificata.

Alla larghezza minima del campo si può associare anche il segno meno (-), che indica l'allineamento a sinistra del valore stampato. Ad esempio, se volessimo stampare due numeri interi con una larghezza minima del campo di 5, ma allineati a sinistra:

int num1 = 123;
int num2 = 456;
printf("Numeri: %-5d %-5d\n", num1, num2);

L'output sarà:

Numeri: 123   456

Precisione

Gli specificatori di formato possono includere anche una parte opzionale che indica la precisione (.p).

Il significato del valore di precisione dipende dal tipo di dato che si sta stampando. Nel caso dei numeri in virgola mobile, la precisione indica il numero di cifre da stampare dopo il punto decimale.

Ad esempio, supponiamo di voler stampare il numero in virgola mobile 3.14159 con una precisione di 2 cifre decimali:

double num = 3.14159;
printf("Numero: %.2lf\n", num);

L'output sarà:

Numero: 3.14

In questo caso, il numero 3.14159 viene arrotondato a 3.14, poiché abbiamo specificato una precisione di 2 cifre decimali.

Da notare che nel caso dei numeri in virgola mobile, se la precisione non viene specificata, printf utilizza una precisione predefinita di 6 cifre decimali.

Inoltre, il valore viene arrotondato e non troncato. Ad esempio, se volessimo stampare il numero 2.6789 con una precisione di 3 cifre decimali:

double num = 2.6789;
printf("Numero: %.3lf\n", num);

L'output sarà:

Numero: 2.679

In questo caso, il numero 2.6789 viene arrotondato a 2.679, poiché la quarta cifra decimale (9) è maggiore o uguale a 5.

Gli Specificatori di Formato più Comuni

Come abbiamo visto, la forma più comune di uno specificatore di formato è %-m.pX, dove X indica il tipo di dato da stampare.

Vediamo quali sono gli specificatori di formato più comuni e i loro significati:

  • %d: Stampa un intero decimale con segno (tipo int).

    Quando si adopera questo specificatore, la precisione (.p) indica il numero minimo di cifre da stampare. Se il numero ha meno cifre, verranno aggiunti degli zeri a sinistra.

    Nel caso in cui la precisione viene omessa, essa viene assunta per default pari a 1. Quindi, i seguenti specificatori sono equivalenti: %d e %.1d.

  • %le: Stampa un numero in virgola mobile in notazione scientifica (tipo double).

    La precisione (.p) indica il numero di cifre da stampare dopo il punto decimale. Nel caso in cui la precisione venga omessa, essa viene assunta per default pari a 6. Viceversa, se la precisione è 0, non verrà stampato alcun punto decimale.

    Ad esempio, volendo stampare il numero 12345.6789 con una precisione di 2 cifre decimali in notazione scientifica:

    double num = 12345.6789;
    printf("Numero: %.2le\n", num);
    

    L'output sarà:

    Numero: 1.23e+04
    
  • %lf: Stampa un numero in virgola mobile in notazione decimale (tipo double).

    La precisione (.p) indica il numero di cifre da stampare dopo il punto decimale. Nel caso in cui la precisione venga omessa, essa viene assunta per default pari a 6. Viceversa, se la precisione è 0, non verrà stampato alcun punto decimale.

    Ad esempio, volendo stampare il numero 3.14159 con una precisione di 3 cifre decimali:

    double num = 3.14159;
    printf("Numero: %.3lf\n", num);
    

    L'output sarà:

    Numero: 3.142
    
  • %lg: Stampa un numero in virgola mobile utilizzando la notazione più compatta tra decimale e scientifica (tipo double).

    Con questo specificatore, la precisione (.p) non indica più il numero di cifre decimali, ma il numero totale di cifre significative da stampare.

    Inoltre, a differenza dello specificatore %lf, questo specificatore non aggiunge zeri alla fine del numero per raggiungere la precisione specificata.

    In più, se il numero non ha cifre decimali, non verrà stampato alcun punto decimale.

    Questo specificatore è particolarmente utile quando si vogliono stampare numeri la cui dimensione non è nota a priori oppure quando quest'ultima varia notevolmente. Quando lo si utilizza per stampare numeri non molto grandi o non molto piccoli, si ottiene un output in formato decimale, come se avessimo usato %lf. Viceversa, quando si stampano numeri molto grandi o molto piccoli, si ottiene un output in notazione scientifica, come se avessimo usato %le, salvando, in questo modo, spazio prezioso.

Oltre a questi specificatori di formato, ne esistono molti altri che consentono di stampare diversi tipi di dati, come caratteri, stringhe, numeri esadecimali, ottali, e così via. Questi verranno trattati in modo più approfondito nelle lezioni successive man mano che affronteremo argomenti più avanzati relativi alla funzione printf.

Esempio Completo

Proviamo, adesso, a mettere insieme quanto appreso finora con un esempio che stampa numeri sia interi che in virgola mobile, utilizzando vari specificatori di formato.

#include <stdio.h>

int main() {
    int a = 42;
    double b = 1024.56789;

    printf("|%d|%5d|%-5d|%5.3d|\n", a, a, a, a);
    printf("|%10.3lf|%10.3e|%-10.3lg|\n", b, b, b);

    return 0;
}

Se proviamo a compilare ed eseguire questo programma, otterremo il seguente output:

|42|   42|42   |  042|
|  1024.568| 1.025e+03|1.02e+03  |

In questo esempio, abbiamo definito due variabili: a di tipo int e b di tipo double. Successivamente, abbiamo utilizzato la funzione printf per stampare i valori di queste variabili con diversi specificatori di formato.

Abbiamo usato il carattere | per delimitare visivamente i campi stampati, in modo da evidenziare l'effetto della larghezza minima del campo e dell'allineamento.

Adesso, vediamo nel dettaglio i singoli specificatori di formato utilizzati:

  • %d: Stampa il valore di a come un intero decimale con segno utilizzando il minimo spazio possibile.
  • %5d: Stampa il valore di a con una larghezza minima del campo di 5, allineato a destra. Poiché a ha solo 2 cifre, verranno aggiunti 3 spazi vuoti a sinistra.
  • %-5d: Stampa il valore di a con una larghezza minima del campo di 5, allineato a sinistra. Poiché a ha solo 2 cifre, verranno aggiunti 3 spazi vuoti a destra.
  • %5.3d: Stampa il valore di a con una larghezza minima del campo di 5 e una precisione di 3 cifre. Poiché a ha solo 2 cifre, verrà aggiunto uno zero a sinistra per raggiungere la precisione di 3, e verranno aggiunti 2 spazi vuoti a sinistra per raggiungere la larghezza minima di 5.
  • %10.3lf: Stampa il valore di b come un numero in virgola mobile con una larghezza minima del campo di 10 e una precisione di 3 cifre decimali. Poiché b ha 7 cifre prima del punto decimale e 5 cifre decimali, verranno aggiunti 2 spazi vuoti a sinistra per raggiungere la larghezza minima di 10.
  • %10.3e: Stampa il valore di b in notazione scientifica con una larghezza minima del campo di 10 e una precisione di 3 cifre decimali. Anche in questo caso, verranno aggiunti 2 spazi vuoti a sinistra per raggiungere la larghezza minima di 10.
  • %-10.3lg: Stampa il valore di b utilizzando la notazione più compatta tra decimale e scientifica, con una larghezza minima del campo di 10 e una precisione di 3 cifre significative, allineato a sinistra. Poiché b viene stampato in notazione scientifica con 4 cifre significative, verranno aggiunti 6 spazi vuoti a destra per raggiungere la larghezza minima di 10.

In questo modo, l'esempio dimostra come utilizzare la funzione printf con vari specificatori di formato per controllare l'aspetto dell'output stampato sullo schermo.

Introduzione alle Sequenze di Escape

Concludiamo questa introduzione alla funzione printf parlando delle sequenze di escape.

Spesso, nei programmi visti sinora, abbiamo inserito, al termine delle stringhe di formato, la sequenza di escape \n. Questa sequenza speciale indica a printf di andare a capo dopo aver stampato il contenuto della stringa.

Questa sequenza fa parte di un insieme più ampio di stringhe che prendono collettivamente il nome di sequenze di escape. Esse sono utilizzate per rappresentare caratteri speciali all'interno delle stringhe, che altrimenti non potrebbero essere inseriti direttamente dalla tastiera oppure avrebbero un significato diverso per il compilatore, come ad esempio le virgolette doppie (").

Esistono numerose sequenze di escape, che vedremo in dettaglio più avanti, ma per ora ci basti conoscere le più comuni:

  • \n
  • \t
  • \a
  • \b

Quando la funzione printf incontra una sequenza di escape all'interno della stringa di formato, essa interpreta la sequenza come un'azione da svolgere, piuttosto che come una semplice serie di caratteri da stampare.

Per cui, quando incontra la sequenza \n, printf va a capo, spostando il cursore alla riga successiva. Allo stesso modo, la sequenza \t inserisce una tabulazione orizzontale, spostando il cursore alla posizione successiva di tabulazione. La sequenza \a produce un segnale acustico (un "beep") su alcuni terminali, mentre la sequenza \b sposta il cursore indietro di un carattere, cancellando il carattere precedente.

Il nome escape (che in inglese significa "fuga") deriva dal fatto che queste sequenze permettono di "fuggire" dal normale comportamento di stampa dei caratteri, consentendo di inserire caratteri speciali o di controllare il formato dell'output in modi che altrimenti non sarebbero possibili.

Una stringa di formato passata alla funzione printf può contenere più sequenze di escape, e queste possono essere combinate con testo normale e specificatori di formato per creare output complessi e ben formattati.

Ad esempio, consideriamo il seguente codice:

printf("Ciao,\nMondo!\n");
printf("Colonna1\tColonna2\tColonna3\n");
printf("Attenzione!\a\n");

L'output di questo codice sarà:

Ciao,
Mondo!
Colonna1    Colonna2    Colonna3
Attenzione!

In questo esempio, la prima chiamata a printf utilizza la sequenza di escape \n per andare a capo dopo "Ciao," e dopo "Mondo!". La seconda chiamata utilizza la sequenza di escape \t per inserire tabulazioni tra le colonne. La terza chiamata utilizza la sequenza di escape \a per produrre un segnale acustico prima di stampare "Attenzione!".

Esistono anche alcune sequenze di escape che consentono di stampare caratteri speciali all'interno delle stringhe, come ad esempio:

  • \\: Stampa una barra rovesciata (\).
  • \": Stampa una virgoletta doppia (").

Infatti, poiché la barra rovesciata (\) è il carattere di escape, per stampare una barra rovesciata stessa è necessario utilizzare la sequenza \\. Allo stesso modo, per stampare una virgoletta doppia all'interno di una stringa racchiusa tra virgolette doppie, è necessario utilizzare la sequenza \".

Ad esempio, consideriamo il seguente codice:

printf("Percorso del file: C:\\Documenti\\File.txt\n");
printf("Citazione: \"La conoscenza è potere.\"\n");

L'output di questo codice sarà:

Percorso del file: C:\Documenti\File.txt
Citazione: "La conoscenza è potere."

In questo esempio, la prima chiamata a printf utilizza la sequenza di escape \\ per stampare le barre rovesciate nel percorso del file su un sistema Windows. La seconda chiamata utilizza la sequenza di escape \" per stampare le virgolette doppie intorno alla citazione.