Specificatori di Formato per l'Input in Linguaggio C

Le funzioni di input della famiglia scanf (scanf, fscanf, sscanf, ecc.) rassomigliano alle funzioni di output della famiglia printf. Entrambe le famiglie adoperano specificatori di formato per indicare il tipo di dati da leggere o scrivere.

Tuttavia, questa somiglianza può essere fuorviante, perché il funzionamento è diverso. Conviene pensare alle funzioni della famiglia scanf come a delle funzioni di pattern-matching: esse confrontano i caratteri letti dall'input con il formato specificato e, quando trovano una corrispondenza, convertono i dati letti nel tipo di dato appropriato e li memorizzano nelle variabili indicate.

In questo modo, le stringhe di formato passate in input rappresentano dei modelli (pattern) che tali funzioni cercano di far corrispondere ai dati letti dall'input.

In questa lezione ci concentreremo sul funzionamento degli specificatori di formato per l'input in linguaggio C.

Concetti Chiave
  • Le funzioni di input della famiglia scanf utilizzano stringhe di formato per specificare il tipo di dati da leggere.
  • Le stringhe di formato per l'input possono contenere specificatori di formato, caratteri di spaziatura e caratteri non di spaziatura.
  • Gli specificatori di formato per l'input hanno una struttura composta da un carattere di percentuale %, un soppressore di assegnamento opzionale *, una larghezza massima del campo opzionale, un modificatore di dimensione opzionale e un carattere di conversione.
  • Gli specificatori di tipo per l'input indicano il tipo di dato da leggere e includono opzioni per interi, numeri in virgola mobile, caratteri, stringhe e scanset.
  • Gli scanset permettono di definire insiemi di caratteri da leggere o da escludere dall'input.

Stringhe di formato per le funzioni di input

Abbiamo visto che tutte le funzioni di input della famiglia scanf accettano in input una stringa di formato che specifica il tipo di dati da leggere.

Tutte queste funzioni, scanf, fscanf, sscanf, ecc., lavorano, di base, in questo modo:

  1. La funzione cerca il pattern all'interno dell'input;
  2. Nel caso in cui l'input non corrisponde al pattern, la funzione ritorna non appena rileva la non-corrispondenza;
  3. Il primo carattere che non corrisponde al pattern viene rimesso nell'input (push-back); ciò significa che esso verrà riletto da future operazioni o funzioni di input.

Una stringa di formato per le funzioni di input può contenere i seguenti elementi:

  • Specificatori di formato:

    Gli specificatori di formato all'interno della stringa di formato per le funzioni di input rassomigliano a quelli adoperati nelle funzioni di output (famiglia printf). La maggior parte di essi salta e ignora i caratteri di spaziatura (spazi, tabulazioni, ritorni a capo) che si trovano prima del dato da leggere.

    Tuttavia, essi non consumano (ossia non leggono) i caratteri di spaziatura che si trovano dopo il dato letto. Questi caratteri rimangono nell'input e potranno essere letti da future operazioni o funzioni di input.

    Ad esempio, supponiamo di voler leggere un numero intero. Quindi dobbiamo adoperare lo specificatore di formato %d. Se l'input è il seguente (dove abbiamo usato il carattere speciale per rappresentare gli spazi vuoti):

    ␣␣␣42␣␣␣
    

    La funzione di input salterà gli spazi vuoti iniziali e leggerà il numero 42. Tuttavia, gli spazi vuoti che seguono il numero rimarranno nell'input e potranno essere letti successivamente.

    Ciò vale anche se al termine del dato letto c'è un carattere di nuova linea (\n), che viene considerato anch'esso un carattere di spaziatura. Quindi, la prossima lettura dell'input potrebbe leggere quel carattere di nuova linea.

  • Caratteri di spaziatura:

    Un carattere di spaziatura (spazio, tabulazione, nuova linea) all'interno della stringa di formato per le funzioni di input corrisponde a uno o più caratteri di spaziatura nell'input. In altre parole, se nella stringa di formato c'è uno spazio, la funzione di input salterà tutti i caratteri di spaziatura presenti nell'input fino a quando non incontrerà un carattere non di spaziatura. Ciò vale anche se non ce ne sono.

  • Caratteri non di spaziatura:

    Un carattere non di spaziatura (ad esempio, una lettera o un numero) all'interno della stringa di formato per le funzioni di input deve corrispondere esattamente a un carattere non di spaziatura identico nell'input. Se c'è una discrepanza, la funzione di input si fermerà e ritornerà il numero di elementi letti fino a quel momento.

Per chiarire meglio questi concetti, consideriamo un esempio pratico. Supponiamo di voler leggere dall'input un codice ISBN di un libro.

Il codice ISBN (International Standard Book Number) è un identificatore numerico univoco per i libri, composto da 13 cifre divise in gruppi separati da trattini. Un esempio di codice ISBN è il seguente:

ISBN 123-4-56-789012-3

Per leggere questo codice ISBN dall'input, possiamo utilizzare la seguente stringa di formato:

"ISBN %d-%d-%d-%d-%d"

In questa stringa di formato:

  1. ISBN: I caratteri I, S, B, N devono corrispondere esattamente ai caratteri nell'input.
  2. Dopo ISBN, c'è uno spazio, che corrisponde a uno o più caratteri di spaziatura nell'input.
  3. %d: Questo specificatore di formato indica che dobbiamo leggere un numero intero.
  4. -: Il trattino deve corrispondere esattamente al carattere - nell'input.
  5. Questo schema si ripete per gli altri gruppi di cifre.

Specificatori di formato per l'input

Gli specificatori di formato per l'input in linguaggio C sono leggermente più semplici rispetto a quelli per l'output.

La struttura generale di uno specificatore di formato per l'input è la seguente:

Struttura di uno specificatore di formato per l'input
Figura 1: Struttura di uno specificatore di formato per l'input

Si tratta di una struttura composta da:

  • Un carattere di percentuale %, che indica l'inizio dello specificatore di formato.
  • Un asterisco opzionale *, che indica che il dato letto non deve essere memorizzato in una variabile.
  • Una larghezza massima del campo opzionale, che specifica il numero massimo di caratteri da leggere.
  • Un modificatore di dimensione opzionale, che indica la dimensione del tipo di dato da leggere (ad esempio, l per long, h per short, ecc.).
  • Un carattere di conversione, che indica il tipo di dato da leggere (ad esempio, d per interi decimali, f per numeri in virgola mobile, s per stringhe, ecc.).

Analizziamo i componenti di questa struttura in dettaglio.

Soppressore di assegnamento (*)

Il soppressore di assegnamento * è un componente opzionale dello specificatore di formato per l'input. Quando viene utilizzato, indica alla funzione di input di leggere il dato corrispondente ma di non memorizzarlo in nessuna variabile. Questo può essere utile quando si desidera ignorare determinati dati nell'input.

Inoltre, e questo è un dettaglio importante, l'uso del soppressore di assegnamento * non influisce sul conteggio del numero di elementi letti che la funzione di input restituisce. In altre parole, nel conteggio finale, gli elementi letti con il soppressore di assegnamento * non vengono considerati.

Massima larghezza del campo

Il campo di massima larghezza del campo è un componente opzionale dello specificatore di formato per l'input. Esso consente di specificare il numero massimo di caratteri da leggere per un dato particolare.

Quando viene specificata una massima larghezza del campo, la funzione di input funziona in questo modo:

  1. Ignora tutti gli spazi bianchi iniziali (spazi, tabulazioni, nuove linee) senza conteggiarli nel limite di larghezza del campo.
  2. Inizia a leggere i caratteri fino a raggiungere il limite di larghezza del campo specificato o fino a incontrare un carattere non valido per il tipo di dato in questione.

Modificatori di dimensione

I modificatori di dimensione sono componenti opzionali dello specificatore di formato per l'input che permettono di modificare la dimensione del tipo di dato da leggere. Essi sono utili quando si lavora con tipi di dati di dimensioni diverse, come short, long, long long, ecc.

La tabella che segue riporta tutti i modificatori di dimensione disponibili per gli specificatori di formato per l'input in linguaggio C:

Modificatore di dimensione Specificatori di tipo Tipo di dato corrispondente
hh d, i signed char *
hh u, o, x, X, n unsigned char *
h d, i short int *
h u, o, x, X, n unsigned short int *
l d, i long int *
l u, o, x, X unsigned long int *
l f, e, E, g, G, a, A double *
l c, s, [ wchar_t *
ll d, i long long int *
ll u, o, x, X, n unsigned long long int *
j d, i intmax_t *
j u, o, x, X, n uintmax_t *
z d, i, u, o, x, X, n size_t *
t d, i, u, o, x, X, n ptrdiff_t *
L f, e, E, g, G, a, A long double *
Tabella 1: Lista completa dei modificatori di dimensione

Specificatori di tipo

Gli specificatori di tipo sono componenti obbligatorie dello specificatore di formato per l'input che indicano il tipo di dato da leggere. Essi determinano come la funzione di input interpreta i caratteri letti dall'input e in quale tipo di dato li converte.

Di seguito è riportata la tabella completa degli specificatori di tipo per l'input in linguaggio C, insieme ai tipi di dato corrispondenti:

Specificatore di tipo Tipo di dato corrispondente Descrizione
d int * Corrisponde a un intero decimale con segno.
i int * Corrisponde a un intero; tuttavia, a seconda del prefisso, può essere interpretato come decimale, ottale o esadecimale. Se inizia con 0, è ottale; se inizia con 0x o 0X, è esadecimale; altrimenti, è decimale.
o unsigned int * Corrisponde a un intero ottale senza segno.
u unsigned int * Corrisponde a un intero decimale senza segno.
x, X unsigned int * Corrisponde a un intero esadecimale senza segno. Non fa distinzione tra maiuscole e minuscole.
f, F, e, E, g, G, a, A float * Corrisponde a un numero in virgola mobile a precisione singola. Con questo specificatore, si possono inserire anche i valori NaN e Infinity.
c char * Corrisponde a n caratteri, dove n è la larghezza del campo specificata (o 1 se non specificata). Non ignora gli spazi bianchi e, inoltre, non aggiunge il carattere di terminazione null (\0) alla fine. Pretende un puntatore a un array di caratteri di dimensione sufficiente per contenere i caratteri letti.
s char * Corrisponde a una stringa di caratteri composta da caratteri non di spaziatura. Ignora gli spazi bianchi iniziali e aggiunge il carattere di terminazione null (\0) alla fine della stringa letta. Pretende un puntatore a un array di caratteri di dimensione sufficiente per contenere la stringa letta.
[ char * Corrisponde a una sequenza di caratteri che appartengono ad uno scanset (insieme di scansione) specificato tra parentesi quadre (vedere sotto). Aggiunge il carattere di terminazione null (\0) alla fine della stringa letta. Pretende un puntatore a un array di caratteri di dimensione sufficiente per contenere la stringa letta.
p void ** Corrisponde a un puntatore memorizzato in formato esadecimale.
n int * Non corrisponde a nessun input. Invece, memorizza il numero di caratteri letti fino a quel momento nell'argomento corrispondente. Inoltre, non incrementa il conteggio del numero di elementi letti restituito dalla funzione di input.
% N/A Corrisponde al carattere % nell'input. Non consuma alcun dato e non incrementa il conteggio del numero di elementi letti restituito dalla funzione di input.
Tabella 2: Lista completa degli specificatori di tipo per l'input

Un dettaglio importante da tenere a mente è che tutti i valori numerici possono iniziare con un segno positivo (+) o negativo (-), anche nel caso degli specificatori di tipo senza segno (u, o, x, X). Tuttavia, se si inserisce un segno negativo con uno di questi specificatori, il numero verrà interpretato come un valore molto grande, poiché verrà trattato come un numero senza segno.

Nota

Attenzione a far corrispondere gli specificatori di tipo con variabili del tipo corretto

Nel caso delle funzioni della famiglia scanf (fscanf, sscanf ecc.) è ancora più importante (rispetto alle funzioni della famiglia printf) far corrispondere gli specificatori di tipo a variabili del tipo corretto.

In caso contrario, dal momento che tali funzioni richiedono gli indirizzi degli argomenti passati, il risultato è indefinito e potrebbe provocare il crash del programma.

Gli Scanset

Adesso, approfondiamo uno specificatore di tipo particolare per l'input che abbiamo solo accennato in precedenza: lo specificatore di tipo [ (scanset).

Si tratta di uno specificatore più complesso e più flessibile dello specificatore di tipo s (stringa). Infatti, con lo scanset è possibile definire un insieme di caratteri che si desidera leggere dall'input.

Questo specificatore di tipo ha due forme principali:

  • %[insieme]
  • %[^insieme]

In questo caso, insieme rappresenta una serie di caratteri racchiusi tra parentesi quadre. Questo specificatore di tipo funziona in questo modo:

  • %[insieme]: Legge una sequenza di caratteri dall'input che appartengono all'insieme specificato. La lettura continua fino a quando non si incontra un carattere che non appartiene all'insieme.
  • %[^insieme]: Legge una sequenza di caratteri dall'input che non appartengono all'insieme specificato. La lettura continua fino a quando non si incontra un carattere che appartiene all'insieme.

Chiariamo con alcuni esempi pratici.

Supponiamo di voler leggere una stringa di caratteri composta solo dalle lettere a, b e c. Possiamo utilizzare il seguente specificatore di tipo:

char str[100];
scanf("%[abc]", str);

Adesso, se passiamo in input la stringa abacabadab, la funzione di input leggerà solo i caratteri abacaba e si fermerà quando incontrerà il carattere d, che non appartiene all'insieme specificato.

L'insieme dei caratteri può anche essere negato. Ad esempio, supponiamo di voler leggere una stringa di caratteri che non contenga le lettere x, y e z. Possiamo utilizzare il seguente specificatore di tipo:

char str[100];
scanf("%[^xyz]", str);

In questo caso, se passiamo in input la stringa helloworldxyz, la funzione di input leggerà solo i caratteri helloworld e si fermerà quando incontrerà il carattere x, che appartiene all'insieme negato.

Un altro aspetto interessante degli scanset è che è possibile specificare intervalli di caratteri utilizzando il trattino -. Anche se si tratta di una estensione non standard del linguaggio C, molti compilatori la supportano. Ad esempio, per leggere tutte le lettere minuscole dell'alfabeto, possiamo utilizzare il seguente specificatore di tipo:

char str[100];
scanf("%[a-z]", str);

Quando si usa il trattino, però, bisogna fare attenzione a posizionarlo correttamente. Infatti:

  • Se il trattino si trova all'inizio o alla fine dell'insieme, viene interpretato come un carattere normale.
  • Se il trattino si trova tra due caratteri, viene interpretato come un intervallo di caratteri.

Quindi, ad esempio, per leggere tutte le lettere minuscole e il carattere -, possiamo utilizzare il seguente specificatore di tipo:

char str[100];
scanf("%[-a-z]", str);

Vediamo un altro esempio in cui vogliamo leggere una stringa di caratteri che contenga solo cifre numeriche. Possiamo utilizzare il seguente specificatore di tipo:

char str[100];
scanf("%[0-9]", str);

L'ultimo dettaglio riguardante gli scanset è che, per includere il carattere ] nell'insieme, esso deve essere posizionato come primo carattere dopo il simbolo ^ (se presente) o come primo carattere dell'insieme. Ad esempio:

char str[100];
scanf("%[]abc]", str);  // Include ']' nell'insieme
scanf("%[^]abc]", str); // Include ']' nell'insieme negato

In caso contrario, il carattere ] verrà interpretato come la fine dell'insieme.

Esempi

Concludiamo questa lezione con alcuni esempi di applicazione degli specificatori di formato. Per rendere più chiaro il comportamento useremo il carattere per rappresentare un qualunque carattere di spaziatura.

Esempio

Esempio 1

n = scanf("%d%d", &a, &b);

con input

\texttt{␣␣42␣,␣␣73␣␣}

Risultato:

n=1
a=42
b non modificato
{\color{red}{\texttt{␣␣42␣}}}\texttt{,␣␣73␣␣}

In rosso è evidenziato l'input che è stato effettivamente letto dalla funzione scanf.

Il primo intero viene letto correttamente, ma la funzione si ferma al carattere , che non corrisponde allo specificatore di formato %d.

Esempio

Esempio 2

n = scanf("%d,%d", &a, &b);

con input:

\texttt{␣␣42␣,␣␣73␣␣}

Risultato:

n=1
a=42
b non modificato
{\color{red}{\texttt{␣␣42␣}}}\texttt{,␣␣73␣␣}

Il primo intero viene letto correttamente, ma la funzione si ferma al carattere di spaziatura dopo la virgola, che non corrisponde al carattere , nello specificatore di formato.

Esempio

Esempio 3

n = scanf("%d ,%d", &a, &b);

con input:

\texttt{␣␣42␣,␣␣73␣␣}

Risultato:

n=2
a=42
b=73
{\color{red}{\texttt{␣␣42␣,␣␣73}}}\texttt{␣␣}

Entrambi gli interi vengono letti correttamente, poiché lo spazio nello specificatore di formato consente di saltare i caratteri di spaziatura nell'input.

Esempio

Esempio 4

n = scanf("%*d%d", &a);

con input:

\texttt{12␣34␣}

Risultato:

n=1
a=34
{\color{red}{\texttt{12␣34}}}\texttt{␣}

Il primo intero viene letto ma non memorizzato a causa del soppressore di assegnamento *. Il secondo intero viene letto e memorizzato nella variabile a.

Esempio

Esempio 5

n = scanf("%*s%s", str);

con input:

\texttt{Ciao␣come␣stai?}

Risultato:

n=1
str="come"
{\color{red}{\texttt{Ciao␣come}}}\texttt{␣stai?}

La prima stringa viene letta ma non memorizzata a causa del soppressore di assegnamento *. La seconda stringa viene letta e memorizzata nella variabile str. Notare che i caratteri di spaziatura vengono considerati come terminatori di stringa.

Esempio

Esempio 6

n = scanf("%1d%2d%3d", &a, &b, &c);

con input:

\texttt{12345␣}

Risultato:

n=3
a=1
b=23
c=45
{\color{red}{\texttt{12345}}}\texttt{␣}

Il primo intero viene letto con una larghezza di campo di 1, quindi varrà 1. Il secondo intero viene letto con una larghezza di campo di 2, quindi varrà 23. Il terzo intero viene letto con una larghezza di campo di 3, ma poiché ci sono solo due cifre rimanenti (45), verrà letto solo 45.

Esempio

Esempio 7

n = scanf("%2d%2s%2d", &a, str, &c);

con input:

\texttt{123456␣}

Risultato:

n=3
a=12
str="34"
c=56
{\color{red}{\texttt{123456}}}\texttt{␣}

Il primo intero viene letto con una larghezza di campo di 2, quindi varrà 12. La stringa viene letta con una larghezza di campo di 2, quindi conterrà i caratteri "34". Il secondo intero viene letto con una larghezza di campo di 2, quindi varrà 56.

Esempio

Esempio 8

n = scanf("%i%i%i", &a, &b, &c);

con input:

\texttt{12␣034␣0x56␣}

Risultato:

n=3
a=12
b=28
c=86
{\color{red}{\texttt{12␣034␣0x56␣}}}\texttt{␣}

Lo specificatore di tipo %i interpreta il primo numero come decimale (12), il secondo come ottale (034 che è 28 in decimale) e il terzo come esadecimale (0x56 che è 86 in decimale). Quando un numero inizia con 0, viene interpretato come ottale, e quando inizia con 0x o 0X, viene interpretato come esadecimale. Ma comunque, tutti i numeri vengono memorizzati come valori interi nelle variabili corrispondenti.

Esempio

Esempio 9

n = scanf("%[0123456789]", str);

con input:

\texttt{123abc␣}

Risultato:

n=1
str="123"
{\color{red}{\texttt{123}}}\texttt{abc␣}

La stringa viene letta fino a quando non si incontra un carattere che non appartiene all'insieme specificato (in questo caso, le cifre da 0 a 9). Quindi, la lettura si ferma al carattere a.

Esempio

Esempio 10

n = scanf("%[^0123456789]", str);

con input:

\texttt{abcd1234␣}

Risultato:

n=1
str="abcd"
{\color{red}{\texttt{abcd}}}\texttt{1234␣}

La stringa viene letta fino a quando non si incontra un carattere che appartiene all'insieme specificato (in questo caso, le cifre da 0 a 9). Quindi, la lettura si ferma al carattere 1.

Esempio

Esempio 11

n = scanf("%*d%d%n", &a, &b);

con input:

\texttt{12␣34␣56␣}

Risultato:

n=1
a=34
b=5
{\color{red}{\texttt{12␣34}}}\texttt{␣56␣}

In questo esempio, il primo intero, 12, viene scartato a causa del soppressore di assegnamento: %*d. Nella variabile a verrà memorizzato il secondo intero, 34. A questo punto, viene adoperato lo specificatore %n che memorizza il numero di caratteri letti sinora nella variabile b, ossia il valore 5.