scanf è lo specchio di printf: stesso header, stringhe di formato simili, direzione opposta. Legge testo dallo standard input, lo converte secondo il formato e salva i risultati tramite i puntatori che le passi.
È anche la funzione che confonde i principianti più di qualsiasi altra in C, perché tre dei suoi comportamenti sono sorprendenti e uno di questi è una falla di sicurezza. Questa pagina li copre tutti e quattro.
La forma di base, e perché la &
Per eseguirlo, digita un numero e premi Invio. &age non è decorazione. Il C passa ogni argomento per valore, quindi se scrivessi scanf("%d", age) la funzione riceverebbe una copia del valore attuale (spazzatura) di age e non avrebbe modo di raggiungere la variabile stessa. &age passa l'indirizzo della variabile, attraverso cui scanf poi scrive. Consulta i puntatori per il quadro completo.
Dimenticare la & è in assoluto l'errore più comune con scanf. Il compilatore con -Wall lo intercetta (format '%d' expects argument of type 'int *'); senza avvisi attivi compila e corrompe la memoria a un indirizzo casuale.
Leggere più valori
Una sola chiamata può leggere più valori. Gli spazi nell'input, cioè spazi, tabulazioni e a capo, li separano, e gli specificatori numerici ne saltano qualsiasi quantità.
Gli spazi letterali in "%d %d %d" in realtà sono superflui, perché %d salta già gli spazi iniziali, ma rendono il formato leggibile. Quello che non è superfluo è controllare il valore di ritorno, di cui parliamo più avanti.
I caratteri del formato che non sono spazi devono corrispondere esattamente all'input, ed è così che si legge un input strutturato:
Il : nel formato pretende un : nell'input. Digita 9:30 e funziona; digita 9 30 e fallisce dopo aver letto il 9.
Controlla sempre il valore di ritorno
scanf restituisce il numero di elementi assegnati con successo: non il numero di caratteri, e nemmeno un flag di successo. Restituisce EOF se l'input è finito prima che venisse convertito qualcosa.
Digita hello al prompt. scanf restituisce 0, n resta intatta, cioè contiene ancora la spazzatura con cui è partita, e il testo incriminato è ancora nello stream di input. Quest'ultimo punto è ciò che trasforma un singolo input sbagliato in un ciclo infinito:
/* BUG: gira all'infinito con input non numerico */
while (scanf("%d", &n) != 1) {
printf("Riprova: ");
}
scanf non consuma mai il testo che non è riuscita a convertire, quindi la chiamata successiva fallisce sugli stessi caratteri, per sempre. Per recuperare devi scartare tu il resto della riga:
Il controllo su EOF è importante: quando lo stream di input finisce (l'utente preme Ctrl+D, oppure un file in pipe si esaurisce), scanf da lì in poi restituisce sempre EOF, quindi un ciclo che controlla solo != 1 riprova per l'eternità. Tratta "input sbagliato" e "input finito" come i due casi diversi che sono. Con questo sistemato, la funzione discard_line merita di restare in qualsiasi programma che usa scanf su input umano.
L'overflow del buffer con %s
%s legge caratteri dentro un array di char. Nota che non c'è la &: il nome di un array decade già a puntatore al suo primo elemento, quindi &name sarebbe del tipo sbagliato.
Il problema è che %s da solo non ha idea di quanto sia grande il tuo array:
char name[10];
scanf("%s", name); /* PERICOLOSO: digita 40 caratteri, scrive 41 byte */
Niente ferma la scrittura alla fine dell'array. Prosegue su qualunque cosa venga dopo in memoria, altre variabili o l'indirizzo di ritorno: è il classico buffer overflow, storicamente la classe di bug più sfruttata in C. La soluzione è una larghezza tra il % e la s, pari a uno in meno della dimensione dell'array per lasciare spazio al terminatore '\0':
La larghezza va scritta come letterale nella stringa di formato, il che rende scomodo tenerla allineata alla dimensione dell'array: un motivo in più per cui la maggior parte del C in produzione usa invece fgets.
La seconda sorpresa di %s: si ferma al primo spazio. Digita Ada Lovelace e name contiene Ada; Lovelace aspetta nello stream la lettura successiva. %s legge una parola, non una riga.
Il bug dell'a capo rimasto
È il famoso "scanf non funziona" in cui inciampano tutti. Gli specificatori numerici saltano gli spazi iniziali; %c no.
Ecco cosa succede senza quello spazio. Digiti 30 e premi Invio. %d consuma il 3 e lo 0 e si ferma all'a capo, lasciando '\n' nel buffer. Lo scanf("%c", &initial) successivo legge quell'a capo come carattere e restituisce subito il controllo: il prompt compare e viene saltato nello stesso istante.
Uno spazio nella stringa di formato di scanf significa "salta qui qualsiasi quantità di spazi", quindi " %c" scavalca l'a capo rimasto e aspetta un carattere vero. Lo spazio serve solo prima di %c e %[; tutti gli altri specificatori saltano gli spazi da soli.
Leggere float e char
Gli specificatori sono in gran parte gli stessi di printf, con una differenza importante: scanf deve conoscere la dimensione esatta della destinazione, perché scrive attraverso un puntatore.
Prova 1.5 2.25 7 x.
%f legge un float, %lf legge un double. In printf stampano allo stesso modo perché i float vengono promossi a double prima della chiamata, ma in scanf non c'è promozione, e usare %f con un double * scrive quattro byte in una variabile da otto, lasciandola corrotta. Qui il %c non ha bisogno dello spazio iniziale perché il %d che lo precede si è già fermato su uno spazio, non su un a capo. (La tabella completa è negli specificatori di formato.)
fgets: l'alternativa robusta
Per tutto ciò che digita una persona, leggi l'intera riga e analizzala dopo. fgets riceve la dimensione del buffer come argomento, quindi fisicamente non può andare in overflow, e quando la riga ci sta consuma anche l'a capo, così non resta nulla che confonda la lettura successiva. (Una riga più lunga del buffer lascia la coda non letta: la mancanza di '\n' in ciò che hai ricevuto è il modo per accorgertene.)
Tre osservazioni su questo schema:
sizeof linepassa la dimensione reale, quindi cambiare la dimensione dell'array non richiede altre modifiche, a differenza del letterale%19s.fgetsconserva l'a capo se la riga ci stava, ed è per questo che c'è il taglio constrcspn. È anche il modo per rilevare una riga troppo lunga per il buffer: nessun'\n'in ciò che hai ricevuto.- Analizza dopo.
strtolper gli interi (può anche dirti dove si è fermata l'analisi),strtodper i numeri in virgola mobile, oppuresscanf(line, "%d %d", &a, &b)per usare un formato di scanf su una stringa che hai già letto in modo sicuro.
sscanf merita una menzione a parte: è scanf che legge da una stringa invece che dallo stream di input, e si combina perfettamente con fgets. Ottieni la comodità di analisi di scanf senza nessuno dei suoi problemi di stream.
Errori comuni
- Dimenticare la
&su un argomento che non è un array. Compila con-Wall. %ssenza larghezza. Un buffer overflow che aspetta solo un input lungo.- Ignorare il valore di ritorno. E poi leggere una variabile che
scanfnon ha mai assegnato. - Ripetere in ciclo uno
scanffallito senza scartare l'input. Un ciclo infinito. %csenza spazio iniziale dopo aver letto un numero. Il bug del prompt saltato.- Usare
%fper undouble. Corruzione silenziosa;scanfvuole%lf. - Aspettarsi che
%slegga una riga. Legge una sola parola delimitata da spazi.
Domande frequenti
Perché scanf ha bisogno della & in C?
Perché il C passa gli argomenti per valore. scanf deve modificare la tua variabile, quindi le serve l'indirizzo della variabile e non una copia del suo valore: scanf("%d", &age). L'unica eccezione è un array di caratteri, il cui nome è già un indirizzo: scanf("%19s", name) non vuole la &.
Perché scanf salta il mio input per un char o una stringa?
Uno scanf("%d", ...) precedente ha lasciato nel buffer di input l'a capo del tasto Invio. %d e %f saltano gli spazi iniziali, ma %c no, quindi legge quell'a capo invece di aspettare. La soluzione è uno spazio nel formato, scanf(" %c", &ch), che dice a scanf di saltare prima gli spazi.
Come si legge una stringa con scanf in modo sicuro in C?
Dai a %s una larghezza massima pari a uno in meno della dimensione del buffer: char name[20]; scanf("%19s", name);. Senza larghezza, %s scrive tutti i caratteri che l'utente digita e supera allegramente la fine dell'array. Ricorda anche che %s si ferma al primo spazio, quindi non può leggere un nome completo che contiene uno spazio.
Meglio scanf o fgets in C?
Usa fgets per tutto ciò che digita una persona. Legge un'intera riga in un buffer di dimensione nota, non può andare in overflow e, quando la riga ci sta, consuma anche l'a capo, così i classici bug dell'input misto spariscono. Analizza poi la riga con sscanf o strtol. scanf va bene per piccoli esercizi e per input generato da macchine e ben formato.