Specificatori di Formato per l'Output in Linguaggio C

La libreria standard del linguaggio C mette a disposizione tutta una serie di funzioni per l'output formattato che appartengono alla famiglia delle funzioni printf. Queste funzioni sono: printf, fprintf, sprintf, snprintf e vprintf.

Nelle lezioni precedenti abbiamo visto come utilizzare la funzione printf per stampare valori di vari tipi di dato, come interi, numeri in virgola mobile e stringhe di caratteri. Così come abbiamo studiato anche fprintf per scrivere su file e sprintf per scrivere su stringhe.

Tutte queste funzioni hanno un comportamento simile e condividono lo stesso meccanismo per specificare il formato di output dei dati. Questo meccanismo si basa sull'utilizzo di specificatori di formato all'interno della stringa di formato passata come primo argomento alle funzioni di output formattato.

Finora, man mano che abbiamo incontrato nuovi tipi di dato, abbiamo visto i relativi specificatori di formato. In questa lezione, invece, li studieremo in modo sistematico e completo.

Concetti Chiave
  • Gli specificatori di formato sono sequenze di caratteri che indicano come un valore deve essere convertito in una rappresentazione testuale durante l'output formattato.
  • La struttura di uno specificatore di formato include: un carattere di inizio %, flag opzionali, una larghezza minima del campo opzionale, una precisione opzionale, modificatori di dimensione opzionali e uno specificatore di tipo obbligatorio.
  • I flag modificano l'aspetto dell'output, la larghezza minima del campo specifica il numero minimo di caratteri da utilizzare, la precisione varia a seconda del tipo di dato e i modificatori di dimensione cambiano la dimensione del tipo di dato.
  • Gli specificatori di tipo indicano il tipo di dato che viene stampato e determinano come il valore deve essere convertito in una rappresentazione testuale.

Struttura di uno specificatore di formato

Una qualunque delle funzioni di output formattato della famiglia printf accetta come primo argomento una stringa di formato.

Quando tali funzioni processano la stringa, esse sostanzialmente producono in output i caratteri della stringa stessa, uno dopo l'altro. I caratteri normali vengono stampati così come sono, mentre le sequenze di escape vengono interpretate e sostituite con il carattere corrispondente (ad esempio, \n viene sostituito con un carattere di nuova linea).

Viceversa, quando la funzione incontra uno specificatore di formato, essa preleva il valore corrispondente dall'elenco degli argomenti passati alla funzione e lo converte in una rappresentazione testuale secondo le regole definite dallo specificatore di formato stesso.

Uno specificatore di formato ha la seguente struttura generale:

Struttura di uno specificatore di formato
Figura 1: Struttura di uno specificatore di formato

Come si può vedere, uno specificatore di formato inizia sempre con il carattere % ed è composto da diverse parti opzionali e da una parte obbligatoria. Studiamole in dettaglio.

Flag dello specificatore di formato

I flag sono caratteri opzionali che modificano l'aspetto dell'output. Uno specificatore di formato può contenere zero o più flag, che devono essere posizionati immediatamente dopo il carattere %.

I flag sono i seguenti:

Flag Descrizione
- Allinea il valore a sinistra all'interno del campo di larghezza specificato. Per default, i valori sono allineati a destra.
+ Forza la visualizzazione del segno + per i numeri positivi e del segno - per i numeri negativi. Per default, solo i numeri negativi mostrano il segno -.
(spazio) Inserisce uno spazio prima dei numeri positivi se non è presente il segno +.
# Modifica il formato di output per alcuni tipi di dato. Per i numeri ottali, aggiunge il prefisso 0 (zero). Per i numeri esadecimali, aggiunge il prefisso 0x o 0X. Per i numeri in virgola mobile, forza la visualizzazione del punto decimale anche se non ci sono cifre decimali. Per lo specificatore %g o %G, impedisce la rimozione degli zeri in coda.
0 Riempie il campo di larghezza specificato con zeri iniziali invece di spazi. Questo flag è ignorato se è presente il flag -. Inoltre, questo flag viene ignorato per gli specificatori d, i, o, u, x, X oppure se è specificata una precisione.
Tabella 1: Flag degli specificatori di formato

Larghezza minima del campo

La larghezza minima del campo è un numero intero opzionale che specifica il numero minimo di caratteri da utilizzare per rappresentare il valore in output.

Il suo comportamento è il seguente:

  1. Se il numero di caratteri necessari per rappresentare il valore è inferiore alla larghezza minima specificata, lo spazio rimanente viene riempito con spazi (o zeri, se è presente il flag 0).
  2. Se il numero di caratteri necessari per rappresentare il valore è maggiore o uguale alla larghezza minima specificata, il valore viene stampato normalmente senza alcun riempimento.

Per specificare la larghezza minima del campo, si inserisce un numero intero positivo immediatamente dopo i flag (se presenti) nello specificatore di formato.

Si può anche utilizzare un asterisco (*) al posto di un numero intero per indicare che la larghezza minima del campo sarà specificata come argomento aggiuntivo alla funzione di output formattato. In questo caso, l'argomento corrispondente deve essere di tipo int e deve precedere il valore da stampare nell'elenco degli argomenti. Ad esempio:

printf("%*d", 5, 42);

In questo esempio, la larghezza minima del campo è specificata come 5, quindi il numero 42 verrà stampato con tre spazi iniziali per raggiungere una larghezza totale di cinque caratteri.

Precisione

La precisione è un numero intero opzionale preceduto sempre da un punto (.).

Il suo significato varia a seconda del tipo di dato che viene stampato:

Tipo di dato Specificatori di Tipo Significato della precisione
Numeri interi d, i, o, u, x, X Numero minimo di cifre da stampare. Se il numero ha meno cifre, viene riempito con zeri iniziali.
Numeri in virgola mobile in notazione decimale o scientifica A, a, E, e, F, f Numero di cifre da stampare dopo il punto decimale. Il valore predefinito è 6. Se la precisione è 0, non viene stampato alcun punto decimale.
Numeri in virgola mobile in notazione compatta G, g Numero massimo di cifre significative da stampare. Il valore predefinito è 6. Se la precisione è 0, viene considerata come 1.
Stringhe di caratteri s Numero massimo di caratteri da stampare dalla stringa. Se la stringa è più lunga, viene troncata.
Puntatori p Non ha effetto. La precisione viene ignorata.
Tabella 2: Significato della precisione in base al tipo di dato

Anche in questo caso, al posto di specificare un numero intero, si può utilizzare un asterisco (*) per indicare che la precisione sarà specificata come argomento aggiuntivo alla funzione di output formattato. L'argomento corrispondente deve essere di tipo int e deve precedere il valore da stampare nell'elenco degli argomenti. Ad esempio:

printf("%.*f", 2, 3.14159);

In questo esempio, la precisione è specificata come 2, quindi il numero 3.14159 verrà stampato come 3.14, con due cifre dopo il punto decimale.

Se, invece, il numero intero che specifica la precisione è assente, viene assunto come valore predefinito 0.

Modificatori di dimensione

I modificatori di dimensione sono sequenze di caratteri opzionali che modificano la dimensione del tipo di dato che viene stampato.

In altre parole, se specifichiamo, ad esempio, un numero intero con lo specificatore %d, il compilatore si aspetta un argomento di tipo int. Tuttavia, se utilizziamo un modificatore di dimensione come l (elle minuscola), lo specificatore diventa %ld e il compilatore si aspetta un argomento di tipo long int. Analogamente, con il modificatore h (acca minuscola), lo specificatore diventa %hd e il compilatore si aspetta un argomento di tipo short int.

Nella tabella che segue sono elencati i modificatori di dimensione, le associazioni con gli specificatori di tipo e il loro significato:

Modificatore di dimensione Specificatori di Tipo Significato
hh d, i signed char
hh o, u, x, X unsigned char
hh n signed char *
h d, i short int
h o, u, x, X unsigned short int
h n short int *
l d, i long int
l o, u, x, X unsigned long int
l n long int *
l c wint_t
l s wchar_t *
l A, a, E, e, F, f, G, g Nessun effetto
ll d, i long long int
ll o, u, x, X unsigned long long int
ll n long long int *
j d, i intmax_t
j o, u, x, X uintmax_t
j n intmax_t *
z d, i size_t
z o, u, x, X size_t
z n size_t *
t d, i ptrdiff_t
t o, u, x, X ptrdiff_t
t n ptrdiff_t *
L A, a, E, e, F, f, G, g long double
Tabella 3: Modificatori di dimensione degli specificatori di formato

Specificatore di tipo

Lo specificatore di tipo è l'ultima parte di uno specificatore di formato ed è obbligatorio.

Lo specificatore di tipo indica il tipo di dato che viene stampato e determina come il valore deve essere convertito in una rappresentazione testuale.

Nella tabella che segue sono elencati tutti gli specificatori di tipo disponibili, insieme al tipo di dato corrispondente e a una breve descrizione del loro utilizzo:

Specificatore di Tipo Tipo di dato corrispondente Descrizione
d, i int Stampa un numero intero con segno in base decimale.
o unsigned int Stampa un numero intero senza segno in base ottale.
u unsigned int Stampa un numero intero senza segno in base decimale.
x, X unsigned int Stampa un numero intero senza segno in base esadecimale (minuscole per x, maiuscole per X).
f, F double Stampa un numero in virgola mobile in notazione decimale. Se non viene specificata la precisione mostra 6 cifre dopo il punto decimale.
e, E double Stampa un numero in virgola mobile in notazione scientifica. Se non viene specificata la precisione mostra 6 cifre dopo il punto decimale. Quando viene utilizzato e, l'esponente è preceduto dalla lettera e; quando viene utilizzato E, l'esponente è preceduto dalla lettera E.
g, G double Stampa un numero in virgola mobile usando il formato f o il formato e. Se l'esponente è minore di -4 o maggiore o uguale alla precisione, viene utilizzato il formato e; altrimenti, viene utilizzato il formato f. La precisione specifica il numero massimo di cifre significative da stampare. Quando viene utilizzato g, le lettere esponenti sono minuscole; quando viene utilizzato G, le lettere esponenti sono maiuscole. Inoltre, questo formato non mostra zeri non significativi alla fine del numero a meno che non sia specificato il flag #.
a, A double Stampa un numero in virgola mobile in notazione esadecimale. In pratica il numero viene rappresentato come [-]0xh.hhhhp±d, dove [-] è il segno meno opzionale, 0x è il prefisso esadecimale, h.hhhh è la parte frazionaria in esadecimale, p indica l'inizio dell'esponente in base 2, e ±d è l'esponente in base 10. Quando viene utilizzato a, le lettere esadecimali sono minuscole; quando viene utilizzato A, le lettere esadecimali sono maiuscole. Se non viene specificata la precisione mostra 13 cifre dopo il punto decimale.
c unsigned int Stampa un intero come un singolo carattere ASCII.
s char * Stampa una stringa di caratteri puntata dall'argomento. Termina la stampa quando incontra il carattere nullo (\0) o quando raggiunge la precisione specificata.
p void * Stampa un puntatore come un indirizzo esadecimale.
n int * Non produce alcun output. Invece, memorizza il numero di caratteri stampati finora nell'argomento puntato che deve essere di tipo puntatore a intero.
% N/A Stampa il carattere %. Non richiede alcun argomento corrispondente.
Tabella 4: Specificatori di tipo degli specificatori di formato

Perché non esiste uno specificatore di formato per il tipo float?

Se osserviamo bene la tabella 3 che raccoglie tutti gli specificatori di tipo supportati dal linguaggio C e osserviamo la tabella 2 che, invece, mostra i modificatori di lunghezza, possiamo notare che non esiste un modo per dire alla funzione printf di stampare un valore float. Possiamo, infatti, stampare valori double con gli specificatori f, F, e, E, g, G, a e A. Possiamo stampare anche valori long double apponendo il modificatore L davanti agli specificatori visti prima. Ma non c'è un modo per stampare valori float.

Sorge spontanea la domanda: perché non si può?

La risposta deriva da due motivi tecnici:

  1. Per prima cosa, le funzioni della famiglia printf sono funzioni che accettano un numero variabile di argomenti.

    Quando si utilizzano funzioni che hanno un numero variabile di argomenti il compilatore, sebbene conosca il prototipo della funzione, non conosce e non può conoscere in anticipo il tipo degli argomenti passati. Il che ci porta al secondo motivo.

  2. Non conoscendo il tipo degli argomenti passati, il compilatore effettua una promozione automatica del tipo come farebbe nel caso di funzioni di cui non conosce il prototipo.

    Abbiamo visto che nello standard C89 non è un errore invocare una funzione di cui il compilatore non conosce il prototipo o che non è stata precedentemente definita. Solo che, in questo caso, il compilatore effettua delle conversioni automatiche di tipo. Come, ad esempio, convertire qualunque valore float in double.

    A partire dallo standard C99, invocare una funzione senza che il compilatore ne conosca la definizione o il prototipo è un errore.

    Tuttavia, le funzioni con un numero variabile di argomenti sono una sorta di area grigia, in quanto il compilatore ne conosce un prototipo parziale.

    Motivo per cui, quando passiamo un float ad una funzione printf, che è una funzione con un numero variabile di argomenti, esso verrà sempre e comunque convertito in un double prima di essere passato alla funzione.

La conseguenza dei punti 1 e 2 è che non è necessario uno specificatore apposito per il tipo float.

La differenza tra gli specificatori f e F

Abbiamo visto che esistono due specificatori di tipo per stampare numeri in virgola mobile in notazione decimale: f e F.

Questi due specificatori mostrano numeri decimali usando un numero di cifre decimali che dipende dalla precisione specificata (o dal valore predefinito di 6 cifre decimali se la precisione non è specificata).

Sebbene entrambi gli specificatori producano lo stesso output per i numeri normali, la differenza tra f e F si manifesta quando si stampano valori speciali come NaN (Not a Number) e Infinity (Infinito).

La differenza nasce dal fatto che la maggior parte dei processori e delle librerie matematiche utilizzano la rappresentazione IEEE 754 per i numeri in virgola mobile. Questo standard permette alle operazioni in virgola mobile di produrre risultati speciali: infinito, infinito negativo e NaN (Not a Number, ovvero "non un numero").

Per esempio, se dividiamo un numero double positivo per zero, il risultato sarà Infinity. Se dividiamo un numero double negativo per zero, il risultato sarà -Infinity. Se eseguiamo un'operazione matematica non definita, come ad esempio dividere zero per zero, il risultato sarà NaN dal momento che il risultato di una tale operazione non è matematicamente definito.

A partire dallo standard C99, gli specificatori f e F gestiscono questi valori speciali in modo diverso:

  • Lo specificatore f stampa nan per NaN, inf per Infinity e -inf per -Infinity, utilizzando lettere minuscole.
  • Lo specificatore F stampa NAN per NaN, INF per Infinity e -INF per -Infinity, utilizzando lettere maiuscole.

Questa differenza può essere importante in contesti in cui la distinzione tra lettere maiuscole e minuscole è significativa, come ad esempio nei file di log o nei report scientifici.

Anche gli specificatori e, E, g e G seguono la stessa convenzione per quanto riguarda la gestione di NaN e Infinity.

Esempi di specificatori di formato

Adesso che abbiamo visto in dettaglio la struttura di uno specificatore di formato, vediamo alcuni esempi pratici di specificatori di formato completi.

Nel mostrare l'output di ciascun esempio, utilizzeremo il carattere per rappresentare uno spazio vuoto, in modo da rendere più chiaro l'effetto dei flag e della larghezza minima del campo.

Come primo esempio, consideriamo la stampa di due numeri interi, 123 e -456, utilizzando lo specificatore di formato %d applicando, però, diversi flag e larghezze minime del campo:

Specificatore di formato Applicazione a 123 Applicazione a -456
%d 123 -456
%8d ␣␣␣␣␣123 ␣␣␣␣-456
%-8d 123␣␣␣␣␣ -456␣␣␣␣
%+8d ␣␣␣␣+123 ␣␣␣␣-456
% 8d ␣␣␣␣␣123 ␣␣␣␣-456
%08d 00000123 -0000456
%-+8d +123␣␣␣␣ -456␣␣␣␣
%+08d +0000123 -0000456
% 08d ␣0000123 -0000456
Tabella 5: Esempi di specificatori di formato per numeri interi

Passiamo ora a un esempio sempre con numeri interi, ma questa volta li mostriamo in diverse basi numeriche utilizzando gli specificatori di formato %o, %x e %X, includendo anche il flag # per mostrare i prefissi appropriati:

Specificatore di formato Applicazione a 123
%o 173
%#o 0173
%x 7b
%#x 0x7b
%X 7B
%#X 0X7B
%8x ␣␣␣␣␣7b
%#8x ␣␣␣0x7b
Tabella 6: Esempi di specificatori di formato per numeri interi in diverse basi

Per altri esempi, rimandiamo alle lezioni sui numeri interi e sui numeri in virgola mobile, dove sono mostrati numerosi esempi di specificatori di formato applicati a tali tipi di dato.

Vediamo, invece, qualche esempio di specificatori di formato applicati a stringhe di caratteri. Supponiamo di avere la stringa "Ciao" e la stringa "Automobile" e di volerle stampare con diversi specificatori di formato:

Specificatore di formato Applicazione a "Ciao" Applicazione a "Automobile"
%s Ciao Automobile
%12s ␣␣␣␣␣␣␣␣Ciao ␣␣Automobile
%-12s Ciao␣␣␣␣␣␣␣␣ Automobile␣␣
%.3s Cia Aut
%12.3s ␣␣␣␣␣␣␣␣␣Cia ␣␣␣␣␣␣␣␣␣Aut
%-12.3s Cia␣␣␣␣␣␣␣␣␣ Aut␣␣␣␣␣␣␣␣␣
Tabella 7: Esempi di specificatori di formato per stringhe di caratteri

Passare Precisione e Larghezza minima del campo come Argomenti

Finora, abbiamo assunto che la precisione e la larghezza minima del campo fossero specificate direttamente all'interno dello specificatore di formato come numeri interi costanti cablati nella stringa di formato.

Tuttavia, la flessibilità delle funzioni di output formattato consente di specificare la precisione e la larghezza minima del campo come argomenti separati, utilizzando l'asterisco (*) all'interno dello specificatore di formato.

Per capire meglio questo concetto, consideriamo un esempio pratico:

printf("%6.4d", x);

In questo esempio, stiamo stampando un numero intero x con una larghezza minima del campo di 6 caratteri e una precisione di 4 cifre.

Possiamo, però, ottenere lo stesso risultato specificando la larghezza minima del campo e la precisione come argomenti separati:

printf("%*.*d", 6, 4, x);

In questo caso, il primo asterisco (*) indica che la larghezza minima del campo sarà specificata come il primo argomento aggiuntivo (in questo caso, 6), mentre il secondo asterisco indica che la precisione sarà specificata come il secondo argomento aggiuntivo (in questo caso, 4). Infine, l'argomento x è il valore da stampare.

Si noti che gli argomenti che specificano la larghezza minima del campo e la precisione devono essere di tipo int e devono precedere il valore da stampare nell'elenco degli argomenti.

Il vantaggio di questo approccio è che consente di determinare dinamicamente la larghezza minima del campo e la precisione in fase di esecuzione, rendendo il codice più flessibile e adattabile a diverse situazioni.

Inoltre, si possono usare delle macro o delle variabili per specificare la larghezza minima del campo e la precisione, rendendo il codice ancora più leggibile e manutenibile.

Ad esempio, ritornando al codice di sopra, potremmo specificare la larghezza minima del campo attraverso una macro che chiamiamo WIDTH e la precisione attraverso una variabile chiamata precision:

#define WIDTH 6
int precision = 4;

printf("%*.*d", WIDTH, precision, x);

Lo specificatore di formato %p per i puntatori

Lo specificatore di formato %p è utilizzato per stampare i puntatori in linguaggio C. Quando si utilizza questo specificatore, la funzione di output formattato converte l'indirizzo di memoria puntato dal puntatore in una rappresentazione esadecimale leggibile.

Ad esempio, supponiamo di avere un puntatore a un intero:

int x = 42;
int *ptr = &x;

Possiamo stampare l'indirizzo di memoria puntato da ptr utilizzando lo specificatore di formato %p come segue:

printf("L'indirizzo di memoria di x è: %p\n", (void *)ptr);

L'output potrebbe essere simile a questo:

L'indirizzo di memoria di x è: 0x7ffee3b8c8ac

Si noti che, per conformarsi allo standard C, è buona pratica convertire il puntatore a void * quando si utilizza lo specificatore di formato %p. Questo garantisce che il puntatore venga interpretato correttamente indipendentemente dal tipo di dato a cui punta.

Questa è una di quelle funzionalità di basso livello che rende il linguaggio C particolarmente potente e flessibile, permettendo agli sviluppatori di lavorare direttamente con gli indirizzi di memoria quando necessario.

Lo specificatore di formato %n

Uno specificatore di formato particolare è %n. A differenza degli altri specificatori di formato, %n non produce alcun output. Invece, esso consente di memorizzare il numero di caratteri stampati fino a quel punto in una variabile.

Ad esempio, consideriamo il seguente codice:

double pi = 3.141592653589793;
double raggio = 5.0;
int count;

printf("L'area del cerchio di raggio %.2f%n è: %.2f\n",
       raggio,
       &count,
       3.14159 * raggio * raggio);

printf("Numero di caratteri stampati prima di %%n: %d\n", count);

L'output di questo codice sarà simile a:

L'area del cerchio di raggio 5.00 è: 78.54
Numero di caratteri stampati prima di %n: 33

In questo esempio, lo specificatore di formato %n viene utilizzato per memorizzare il numero di caratteri stampati fino a quel punto nella variabile count. Dopo la chiamata a printf, count conterrà il valore 33, che rappresenta il numero di caratteri stampati prima di %n.

Da notare che l'argomento corrispondente a %n deve essere un puntatore a un intero (ad esempio, int *, long *, ecc.), e il tipo di intero deve essere coerente con il modificatore di dimensione utilizzato (se presente). Per questo motivo, nell'esempio sopra, abbiamo passato l'indirizzo di count utilizzando l'operatore di indirizzo (&).

Inoltre, lo specificatore di formato %n non produce alcun output, quindi non influisce sulla formattazione della stringa stampata. Il suo utilizzo riguarda principalmente il conteggio dei caratteri stampati in modo programmatico.