Cosa posso usare per la conversione di input invece di scanf?


125

Ho visto molto spesso persone che scoraggiavano gli altri dall'usare scanfe dire che ci sono alternative migliori. Tuttavia, tutto ciò che alla fine vedo è "non usare scanf" o "ecco una stringa di formato corretta" , e mai alcun esempio delle "alternative migliori" menzionate.

Ad esempio, prendiamo questo frammento di codice:

scanf("%c", &c);

Questo legge lo spazio bianco rimasto nel flusso di input dopo l'ultima conversione. La solita soluzione suggerita a questo è di usare:

scanf(" %c", &c);

o non usare scanf.

Dato che scanfè male, quali sono alcune opzioni ANSI C per la conversione di formati di input che di scanfsolito sono in grado di gestire (come numeri interi, numeri in virgola mobile e stringhe) senza usare scanf?

Risposte:


87

I modi più comuni di leggere l'input sono:

  • usando fgetscon una dimensione fissa, che è ciò che viene solitamente suggerito, e

  • utilizzando fgetc, che può essere utile se stai leggendo solo un singolo char.

Per convertire l'input, esistono diverse funzioni che è possibile utilizzare:

  • strtoll, per convertire una stringa in un numero intero

  • strtof/ d/ ld, per convertire una stringa in un numero in virgola mobile

  • sscanf, Che non è così grave come semplicemente utilizzando scanf, anche se ha la maggior parte delle cadute di seguito indicate

  • Non ci sono buoni modi per analizzare un input separato da delimitatore in semplice ANSI C. O utilizzare strtok_rda POSIX o strtok, che non è thread-safe. Puoi anche lanciare la tua variante thread-safe usando strcspne strspn, poiché strtok_rnon comporta alcun supporto speciale per il sistema operativo.

  • Potrebbe essere eccessivo, ma puoi usare lexer e parser ( flexed bisonessere gli esempi più comuni).

  • Nessuna conversione, basta semplicemente usare la stringa


Dal momento che non sono entrato esattamente nel motivo per cui scanf è male nella mia domanda, elaborerò:

  • Con gli identificatori di conversione %[...]e %c, scanfnon consuma spazi bianchi. Ciò apparentemente non è ampiamente noto, come dimostrano i molti duplicati di questa domanda .

  • C'è un po 'di confusione su quando usare l' &operatore unario quando si fa riferimento agli scanfargomenti di (in particolare con le stringhe).

  • È molto facile ignorare il valore restituito da scanf. Ciò potrebbe facilmente causare un comportamento indefinito dalla lettura di una variabile non inizializzata.

  • È molto facile dimenticare di impedire l'overflow del buffer scanf. scanf("%s", str)è proprio così male come, se non peggio, gets.

  • Non è possibile rilevare l'overflow durante la conversione di numeri interi con scanf. In effetti, l'overflow provoca un comportamento indefinito in queste funzioni.



56

Perché è scanfcattivo?

Il problema principale è che scanfnon si è mai pensato di gestire l'input dell'utente. È stato progettato per essere utilizzato con dati "perfettamente" formattati. Ho citato la parola "perfettamente" perché non è del tutto vero. Ma non è progettato per analizzare dati inaffidabili come l'input dell'utente. Per natura, l'input dell'utente non è prevedibile. Gli utenti fraintendono le istruzioni, fanno errori di battitura, premono accidentalmente invio prima che vengano eseguite, ecc. Si potrebbe ragionevolmente chiedere perché una funzione che non dovrebbe essere utilizzata per l'input dell'utente legge stdin. Se sei un utente esperto * nix, la spiegazione non sarà una sorpresa, ma potrebbe confondere gli utenti Windows. Nei sistemi * nix, è molto comune creare programmi che funzionano tramite piping,stdoutstdindel secondo. In questo modo, è possibile assicurarsi che l'output e l'input siano prevedibili. In queste circostanze, scanffunziona davvero bene. Ma quando si lavora con input imprevedibili, si rischiano tutti i tipi di problemi.

Quindi perché non ci sono funzioni standard facili da usare per l'input dell'utente? Si può solo indovinare qui, ma presumo che i vecchi hacker C hardcore semplicemente pensassero che le funzioni esistenti fossero abbastanza buone, anche se sono molto goffe. Inoltre, quando si guardano alle tipiche applicazioni terminali, raramente leggono l'input dell'utente stdin. Molto spesso si passa tutto l'input dell'utente come argomenti della riga di comando. Certo, ci sono eccezioni, ma per la maggior parte delle applicazioni, l'input dell'utente è una cosa molto minore.

Che cosa si può fare?

Il mio preferito è fgetsin combinazione con sscanf. Una volta ho scritto una risposta a riguardo, ma ripubblicherò nuovamente il codice completo. Ecco un esempio con il controllo e l'analisi dell'errore decenti (ma non perfetti). È abbastanza buono per scopi di debug.

Nota

Non mi piace particolarmente chiedere all'utente di inserire due cose diverse su una sola riga. Lo faccio solo quando si appartengono in modo naturale. Come ad esempio printf("Enter the price in the format <dollars>.<cent>: ")e quindi utilizzare sscanf(buffer "%d.%d", &dollar, &cent). Non farei mai qualcosa del genereprintf("Enter height and base of the triangle: ") . Il punto principale dell'utilizzo di fgetsseguito è di incapsulare gli input per garantire che un input non influisca sul successivo.

#define bsize 100

void error_function(const char *buffer, int no_conversions) {
        fprintf(stderr, "An error occurred. You entered:\n%s\n", buffer);
        fprintf(stderr, "%d successful conversions", no_conversions);
        exit(EXIT_FAILURE);
}

char c, buffer[bsize];
int x,y;
float f, g;
int r;

printf("Enter two integers: ");
fflush(stdout); // Make sure that the printf is executed before reading
if(! fgets(buffer, bsize, stdin)) error_function(buffer, 0);
if((r = sscanf(buffer, "%d%d", &x, &y)) != 2) error_function(buffer, r);

// Unless the input buffer was to small we can be sure that stdin is empty
// when we come here.
printf("Enter two floats: ");
fflush(stdout);
if(! fgets(buffer, bsize, stdin)) error_function(buffer, 0);
if((r = sscanf(buffer, "%d%d", &x, &y)) != 2) error_function(buffer, r);

// Reading single characters can be especially tricky if the input buffer
// is not emptied before. But since we're using fgets, we're safe.
printf("Enter a char: ");
fflush(stdout);
if(! fgets(buffer, bsize, stdin)) error_function(buffer, 0);
if((r = sscanf(buffer, "%c", &c)) != 1) error_function(buffer, r);

printf("You entered %d %d %f %c\n", x, y, f, c);

Se fai molti di questi, potrei consigliare di creare un wrapper che scarichi sempre:

int printfflush (const char *format, ...)
{
   va_list arg;
   int done;
   va_start (arg, format);
   done = vfprintf (stdout, format, arg);
   fflush(stdout);
   va_end (arg);
   return done;
}```

In questo modo si eliminerà un problema comune, ovvero la newline finale che può interferire con l'input del nido. Ma ha un altro problema, che è se la linea è più lunga di bsize. Puoi verificarlo con if(buffer[strlen(buffer)-1] != '\n'). Se si desidera rimuovere la nuova riga, è possibile farlo con buffer[strcspn(buffer, "\n")] = 0.

In generale, consiglierei di non aspettarsi che l'utente inserisca un input in un formato strano che dovresti analizzare diverse variabili. Se si desidera assegnare le variabiliheight e width, non chiedere entrambe contemporaneamente. Consentire all'utente di premere Invio tra di loro. Inoltre, questo approccio è molto naturale in un certo senso. Non riceverai mai l'input stdinfino a quando non premi Invio, quindi perché non leggere sempre l'intera riga? Naturalmente questo può comunque portare a problemi se la linea è più lunga del buffer. Mi sono ricordato di menzionare che l'input dell'utente è grosso in C? :)

Per evitare problemi con linee più lunghe del buffer, è possibile utilizzare una funzione che alloca automaticamente un buffer di dimensioni appropriate, è possibile utilizzare getline() . Lo svantaggio è che in seguito sarà necessario freeil risultato.

Intensificare il gioco

Se sei seriamente intenzionato a creare programmi in C con l'input dell'utente, ti consiglio di dare un'occhiata a una libreria come ncurses. Perché allora probabilmente vorrai anche creare applicazioni con alcuni elementi grafici terminali. Sfortunatamente, perderai un po 'di portabilità se lo fai, ma ti dà un controllo molto migliore dell'input dell'utente. Ad esempio, ti dà la possibilità di leggere istantaneamente un tasto invece di aspettare che l'utente prema invio.


Si noti che (r = sscanf("1 2 junk", "%d%d", &x, &y)) != 2non rileva come male il testo non numerico finale.
chux - Ripristina Monica il

1
@chux Risolto% f% f. Cosa intendi con il primo?
Klutt

Con fgets()of "1 2 junk", if((r = sscanf(buffer, "%d%d", &x, &y)) != 2) {non riporta nulla di sbagliato nell'input anche se ha "junk".
chux - Ripristina Monica il

@chux Ah, ora vedo. Bene, quello era intenzionale.
Klutt

1
scanfè pensato per essere usato con dati perfettamente formattati Ma anche questo non è vero. Oltre al problema con "junk", come menzionato da @chux, c'è anche il fatto che un formato simile "%d %d %d"è felice di leggere input da una, due o tre righe (o anche di più, se ci sono righe vuote intervenute), che non c'è il modo di forzare (diciamo) un input a due righe facendo qualcosa di simile "%d\n%d %d", ecc. scanfpotrebbe essere appropriato per l' input di stream formattato , ma non è affatto buono per qualsiasi cosa basata su line.
Steve Summit,

18

scanfè fantastico quando sai che i tuoi input sono sempre ben strutturati e ben educati. Altrimenti...

IMO, ecco i maggiori problemi con scanf:

  • Rischio di overflow del buffer : se non si specifica una larghezza del campo per gli specificatori di conversione %se %[, si rischia un overflow del buffer (il tentativo di leggere più input di un buffer viene dimensionato per essere conservato). Sfortunatamente, non esiste un buon modo per specificarlo come argomento (come con printf): è necessario codificarlo come parte dello specificatore di conversione o eseguire alcuni macro shenanigans.

  • Accetta input che devono essere rifiutati : se stai leggendo un input con l' %didentificatore di conversione e digiti qualcosa del genere 12w4, ti aspetteresti scanf di rifiutare quell'input, ma non lo fa: converte e assegna correttamente 12, lasciando w4nel flusso di input per incasinare la lettura successiva.

Quindi, cosa dovresti usare invece?

Di solito consiglio di leggere tutti gli input interattivi come testo usando fgets- ti permette di specificare un numero massimo di caratteri da leggere alla volta, in modo da poter facilmente prevenire l'overflow del buffer:

char input[100];
if ( !fgets( input, sizeof input, stdin ) )
{
  // error reading from input stream, handle as appropriate
}
else
{
  // process input buffer
}

Una stranezza fgetsè che memorizzerà la nuova riga finale nel buffer se c'è spazio, quindi puoi fare un semplice controllo per vedere se qualcuno ha digitato più input di quanto ti aspettassi:

char *newline = strchr( input, '\n' );
if ( !newline )
{
  // input longer than we expected
}

Il modo in cui lo affronti dipende da te: puoi rifiutare l'intero input fuori mano e assorbire qualsiasi input rimanente con getchar:

while ( getchar() != '\n' ) 
  ; // empty loop

Oppure puoi elaborare l'input ottenuto finora e rileggere. Dipende dal problema che stai cercando di risolvere.

Per tokenizzare l'input (dividerlo in base a uno o più delimitatori), è possibile utilizzare strtok, ma attenzione: strtokmodifica l'input (sovrascrive i delimitatori con il terminatore di stringa) e non è possibile conservarne lo stato (ovvero è possibile " t tokenizzare parzialmente una stringa, quindi iniziare a tokenizzare un'altra stringa, quindi riprendere da dove si era interrotto nella stringa originale). C'è una variante, strtok_sche preserva lo stato del tokenizer, ma AFAIK la sua implementazione è facoltativa (dovrai verificare che __STDC_LIB_EXT1__sia definito per vedere se è disponibile).

Una volta tokenizzato il tuo input, se hai bisogno di convertire stringhe in numeri (ad es. "1234"=> 1234), Hai opzioni. strtole strtodconvertirà le rappresentazioni di stringa di numeri interi e numeri reali nei rispettivi tipi. Ti consentono anche di cogliere il 12w4problema che ho menzionato sopra: uno dei loro argomenti è un puntatore al primo carattere non convertito nella stringa:

char *text = "12w4";
char *chk;
long val;
long tmp = strtol( text, &chk, 10 );
if ( !isspace( *chk ) && *chk != 0 )
  // input is not a valid integer string, reject the entire input
else
  val = tmp;

Se non specifichi una larghezza del campo ... - o una soppressione della conversione (ad esempio %*[%\n], che è utile per gestire righe troppo lunghe più avanti nella risposta).
Toby Speight,

C'è un modo per ottenere le specifiche di runtime delle larghezze di campo, ma non è carino. Alla fine devi costruire la stringa di formato nel tuo codice (magari usando snprintf()),.
Toby Speight,

5
Hai fatto l'errore più comune con isspace()lì - accetta unsigned personaggi rappresentati come int, quindi devi lanciare unsigned charper evitare UB su piattaforme dove charè firmato.
Toby Speight,

9

In questa risposta suppongo che stai leggendo e interpretando righe di testo . Forse stai chiedendo all'utente, che sta scrivendo qualcosa e sta colpendo INVIO. O forse stai leggendo righe di testo strutturato da un file di dati di qualche tipo.

Dato che stai leggendo righe di testo, ha senso organizzare il tuo codice attorno a una funzione di libreria che legge, bene, una riga di testo. La funzione Standard è fgets(), sebbene ce ne siano altre (incluso getline). E poi il passo successivo è interpretare quella riga di testo in qualche modo.

Ecco la ricetta di base per chiamare fgetsper leggere una riga di testo:

char line[512];
printf("type something:\n");
fgets(line, 512, stdin);
printf("you typed: %s", line);

Questo legge semplicemente in una riga di testo e lo stampa di nuovo. Come scritto ha un paio di limitazioni, che vedremo tra un minuto. Ha anche una grande funzionalità: quel numero 512 che abbiamo passato come secondo argomento fgetsè la dimensione dell'array lineche stiamo chiedendo fgetsdi leggere. Questo fatto - che possiamo dire fgetsquanto è permesso leggere - significa che possiamo essere sicuri che fgetsnon traboccherà l'array leggendo troppo in esso.

Quindi ora sappiamo come leggere una riga di testo, ma cosa accadrebbe se volessimo davvero leggere un numero intero, un numero in virgola mobile, un singolo carattere o una sola parola? (Cioè, che cosa succede se la scanfchiamata che stiamo cercando di migliorare era stato utilizzando un identificatore di formato come %d, %f, %c, o %s?)

È facile reinterpretare una riga di testo - una stringa - come una di queste cose. Per convertire una stringa in un numero intero, il modo più semplice (sebbene imperfetto) per farlo è chiamare atoi(). Per convertire in un numero in virgola mobile, c'è atof(). (E ci sono anche modi migliori, come vedremo tra un minuto.) Ecco un esempio molto semplice:

printf("type an integer:\n");
fgets(line, 512, stdin);
int i = atoi(line);
printf("type a floating-point number:\n");
fgets(line, 512, stdin);
float f = atof(line);
printf("you typed %d and %f\n", i, f);

Se si desidera che l'utente digiti un singolo carattere (forse y o ncome risposta sì / no), puoi letteralmente prendere il primo carattere della riga, in questo modo:

printf("type a character:\n");
fgets(line, 512, stdin);
char c = line[0];
printf("you typed %c\n", c);

(Questo ignora, ovviamente, la possibilità che l'utente abbia digitato una risposta multi-carattere; ignora tranquillamente tutti i caratteri extra che sono stati digitati.)

Infine, se si desidera che l'utente digiti una stringa sicuramente no contenente spazi bianchi, se si desidera trattare la riga di input

hello world!

come la stringa "hello" seguita da qualcos'altro (che è ciò che il scanfformato %savrebbe fatto), beh, in quel caso, mi sono un po 'piegato, non è così facile reinterpretare la linea in quel modo, dopo tutto, quindi la risposta a questa parte della domanda dovrà aspettare un po '.

Ma prima voglio tornare a tre cose che ho saltato.

(1) Abbiamo chiamato

fgets(line, 512, stdin);

da leggere nell'array linee dove 512 ha le dimensioni dell'array, linequindi fgetssa di non sovraccaricarlo. Ma per assicurarti che 512 sia il numero giusto (in particolare, per verificare se qualcuno ha modificato il programma per modificarne le dimensioni), devi rileggere ovunque sia linestato dichiarato. Questo è un fastidio, quindi ci sono due modi molto migliori per mantenere sincronizzate le dimensioni. È possibile, (a) utilizzare il preprocessore per creare un nome per la dimensione:

#define MAXLINE 512
char line[MAXLINE];
fgets(line, MAXLINE, stdin);

Oppure, (b) usa l' sizeofoperatore di C :

fgets(line, sizeof(line), stdin);

(2) Il secondo problema è che non abbiamo verificato errori. Quando leggi l'input, dovresti sempre verificare la possibilità di errore. Se per qualsiasi motivo fgetsnon è possibile leggere la riga di testo richiesta, indica questo restituendo un puntatore null. Quindi avremmo dovuto fare cose del genere

printf("type something:\n");
if(fgets(line, 512, stdin) == NULL) {
    printf("Well, never mind, then.\n");
    exit(1);
}

Infine, c'è il problema che, al fine di leggere una riga di testo, fgetslegge i caratteri e li riempie nel tuo array fino a quando non trova il \ncarattere che termina la linea e riempie anche il \npersonaggio nel tuo array . Puoi vederlo se modifichi leggermente il nostro esempio precedente:

printf("you typed: \"%s\"\n", line);

Se eseguo questo e digito "Steve" quando mi viene richiesto, viene stampato

you typed: "Steve
"

Quello "sulla seconda riga è perché la stringa che leggeva e stampava era in realtà"Steve\n" .

A volte quella nuova linea extra non ha importanza (come quando abbiamo chiamato atoioatof , poiché entrambi ignorano qualsiasi input non numerico aggiuntivo dopo il numero), ma a volte importa molto. Così spesso vorremmo togliere quella nuova riga. Ci sono molti modi per farlo, che vedrò tra un minuto. (So ​​di averlo detto molto. Ma tornerò su tutte queste cose, lo prometto.)

A questo punto, potresti pensare: "Pensavo che avessi detto che scanf non andava bene, e che l'altro modo sarebbe molto meglio. Ma fgetssta iniziando a sembrare un fastidio. Chiamare è scanfstato così facile ! Non posso continuare a usarlo? "

Certo, puoi continuare a usare scanf, se vuoi. (E per cose davvero semplici, in un certo senso è più semplice.) Ma, per favore, non venire a piangere da me quando ti fallisce a causa di uno dei suoi 17 capricci e debolezze, o va in un ciclo infinito a causa dell'input del tuo non mi aspettavo, o quando non riesci a capire come usarlo per fare qualcosa di più complicato. E diamo un'occhiata ai fgetsfastidi reali:

  1. Devi sempre specificare la dimensione dell'array. Beh, ovviamente, non è affatto un fastidio: è una caratteristica, perché l'overflow del buffer è una cosa davvero brutta.

  2. Devi controllare il valore di ritorno. In realtà, è un lavaggio, perché per usarlo scanfcorrettamente, devi controllare anche il suo valore di ritorno.

  3. Devi spogliare la \nschiena. Questo, lo ammetto, è un vero fastidio. Vorrei che ci fosse una funzione standard che potrei indicarti che non ha avuto questo piccolo problema. (Per favore nessuno solleva gets.) Ma rispetto a scanf's17 diversi fastidi, prenderò questo fastidio di fgetsogni giorno.

Quindi, come si fa si striscia che a capo? Tre modi:

a) modo ovvio:

char *p = strchr(line, '\n');
if(p != NULL) *p = '\0';

(b) Modo complicato e compatto:

strtok(line, "\n");

Purtroppo questo non funziona sempre.

(c) Un altro modo compatto e leggermente oscuro:

line[strcspn(line, "\n")] = '\0';

E ora che è fuori mano, possiamo tornare a un'altra cosa che ho ignorato: le imperfezioni di atoi()e atof(). Il problema con questi è che non ti danno alcuna indicazione utile di successo o fallimento: ignorano silenziosamente l'input non numerico finale e restituiscono tranquillamente 0 se non c'è alcun input numerico. Le alternative preferite - che presentano anche altri vantaggi - sono strtole strtod. strtolti permette anche di usare una base diversa da 10, il che significa che puoi ottenere l'effetto (tra le altre cose) %oo %xconscanf. Ma mostrare come usare correttamente queste funzioni è una storia in sé, e sarebbe troppo una distrazione da ciò che si sta già trasformando in una narrazione piuttosto frammentata, quindi non parlerò più di loro adesso.

Il resto della narrativa principale riguarda input che potresti cercare di analizzare, il che è più complicato di un singolo numero o personaggio. Cosa succede se si desidera leggere una riga contenente due numeri, o più parole separate da spazi bianchi o punteggiatura di inquadratura specifica? È qui che le cose diventano interessanti e dove le cose probabilmente si stavano complicando se stavi cercando di fare le cose usando scanf, e dove ora ci sono molte più opzioni che hai letto in modo pulito una riga di testo fgets, sebbene l'intera storia su tutte quelle opzioni potremmo probabilmente riempire un libro, quindi potremo solo grattare la superficie qui.

  1. La mia tecnica preferita è quella di spezzare la linea in "parole" separate da spazi, quindi fare qualcosa di più con ogni "parola". Una delle principali funzioni standard per farlo è strtok(che ha anche i suoi problemi e che valuta anche una discussione completamente separata). La mia preferenza è una funzione dedicata per costruire una serie di puntatori per ogni "parola" spezzata, una funzione che descrivo in queste note del corso . Ad ogni modo, una volta che hai "parole", puoi ulteriormente elaborare ciascuna, forse con le stesse atoi/ atof/ strtol/ strtod funzioni che abbiamo già visto.

  2. Paradossalmente, anche se qui abbiamo trascorso un bel po 'di tempo e sforzi per capire come allontanarci scanf, un altro ottimo modo per gestire la linea di testo che abbiamo appena letto fgetsè passarlo sscanf. In questo modo, si ottiene la maggior parte dei vantaggi scanf, ma senza la maggior parte degli svantaggi.

  3. Se la sintassi di input è particolarmente complicata, potrebbe essere opportuno utilizzare una libreria "regexp" per analizzarla.

  4. Infine, puoi utilizzare qualsiasi soluzione di analisi ad hoc adatta a te. Puoi spostarti attraverso la linea di un personaggio alla volta con un char *puntatore che controlla i caratteri che ti aspetti. Oppure puoi cercare caratteri specifici usando funzioni come strchro strrchr, oppure strspnoppure strcspn, o strpbrk. Oppure puoi analizzare / convertire e saltare gruppi di caratteri numerici usando le funzioni strtolo strtodche abbiamo ignorato in precedenza.

C'è ovviamente molto di più che si possa dire, ma spero che questa introduzione possa iniziare.


C'è una buona ragione per scrivere sizeof (line)piuttosto che semplicemente sizeof line? Il primo fa sembrare che linesia un nome di tipo!
Toby Speight,

@TobySpeight Una buona ragione? No, ne dubito. Le parentesi sono la mia abitudine, perché non posso essere disturbato a ricordare se sono gli oggetti o i nomi dei tipi per cui sono richiesti, ma molti programmatori li lasciano fuori quando possono. (Per me è una questione di preferenze personali e di stile, e una cosa piuttosto minore in questo.)
Steve Summit

+1 per l'utilizzo sscanfcome motore di conversione ma per raccogliere (e possibilmente massaggiare) l'input con uno strumento diverso. Ma forse vale la pena menzionarlo getlinein questo contesto.
dmckee --- ex-moderatore gattino

Quando parli di " fscanffastidi reali", intendi fgets? E il fastidio n. 3 mi infastidisce molto, soprattutto dato che scanfrestituisce un puntatore inutile al buffer piuttosto che restituire il numero di caratteri immessi (il che renderebbe molto più pulito lo strappo dalla newline).
Supercat,

1
Grazie per la spiegazione del tuo sizeofstile. Per me, ricordare quando hai bisogno delle parentesi è facile: penso (type)che sia come un cast senza valore (perché siamo interessati solo al tipo). Un'altra cosa: dici che strtok(line, "\n")non sempre funziona, ma non è ovvio quando potrebbe non funzionare. Immagino che stai pensando al caso in cui la linea era più lunga del buffer, quindi non abbiamo una nuova riga e strtok()restituisce null? È un vero peccato fgets()non restituire un valore più utile in modo che possiamo sapere se la newline è presente o meno.
Toby Speight,

7

Cosa posso usare per analizzare l'input invece di scanf?

Invece di scanf(some_format, ...), considera fgets()consscanf(buffer, some_format_and %n, ...)

Usando " %n", il codice può semplicemente rilevare se tutto il formato è stato scansionato con successo e che alla fine non era presente alcun junk extra di spazi bianchi.

// scanf("%d %f fred", &some_int, &some_float);
#define EXPECTED_LINE_MAX 100
char buffer[EXPECTED_LINE_MAX * 2];  // Suggest 2x, no real need to be stingy.

if (fgets(buffer, sizeof buffer, stdin)) {
  int n = 0;
  // add ------------->    " %n" 
  sscanf(buffer, "%d %f fred %n", &some_int, &some_float, &n);
  // Did scan complete, and to the end?
  if (n > 0 && buffer[n] == '\0') {
    // success, use `some_int, some_float`
  } else {
    ; // Report bad input and handle desired.
  }

6

Dichiariamo i requisiti di analisi come:

  • input valido deve essere accettato (e convertito in qualche altra forma)

  • l'input non valido deve essere rifiutato

  • quando qualsiasi input viene rifiutato, è necessario fornire all'utente un messaggio descrittivo che spieghi (in modo chiaro il linguaggio "facilmente comprensibile per le persone normali che non sono programmatori") perché è stato rifiutato (in modo che le persone possano capire come risolvere il problema)

Per mantenere le cose molto semplici, consideriamo di analizzare un singolo intero decimale semplice (che è stato digitato dall'utente) e nient'altro. Le possibili ragioni per cui l'input dell'utente deve essere rifiutato sono:

  • l'input conteneva caratteri inaccettabili
  • l'input rappresenta un numero inferiore al minimo accettato
  • l'input rappresenta un numero superiore al massimo accettato
  • l'input rappresenta un numero che ha una parte frazionaria diversa da zero

Definiamo anche "input contenuti caratteri inaccettabili" correttamente; e dire che:

  • gli spazi bianchi iniziali e gli spazi bianchi finali verranno ignorati (ad es. "
    5" verrà trattato come "5")
  • è consentito zero o un punto decimale (ad es. "1234." e "1234.000" sono entrambi trattati come "1234")
  • deve esserci almeno una cifra (es. "." è rifiutato)
  • non è consentito più di un punto decimale (ad es. "1.2.3" è rifiutato)
  • le virgole che non sono tra le cifre verranno rifiutate (ad es. ", 1234" viene rifiutato)
  • le virgole che seguono un punto decimale verranno rifiutate (ad es. "1234.000.000" viene rifiutato)
  • le virgole che sono dopo un'altra virgola vengono rifiutate (ad es. "1,, 234" viene rifiutato)
  • tutte le altre virgole verranno ignorate (ad es. "1.234" verrà trattato come "1234")
  • un segno meno che non è il primo carattere non bianco viene rifiutato
  • un segno positivo che non è il primo carattere non bianco viene rifiutato

Da questo possiamo determinare che sono necessari i seguenti messaggi di errore:

  • "Carattere sconosciuto all'inizio dell'input"
  • "Carattere sconosciuto alla fine dell'input"
  • "Carattere sconosciuto nel mezzo dell'input"
  • "Il numero è troppo basso (il minimo è ....)"
  • "Il numero è troppo alto (il massimo è ....)"
  • "Il numero non è un numero intero"
  • "Troppi punti decimali"
  • "Nessuna cifra decimale"
  • "Virgola non valida all'inizio del numero"
  • "Virgola non valida alla fine del numero"
  • "Virgola non valida nel mezzo del numero"
  • "Virgola non valida dopo il punto decimale"

Da questo punto possiamo vedere che una funzione adatta per convertire una stringa in un numero intero dovrebbe distinguere tra tipi di errori molto diversi; e che qualcosa come " scanf()" o " atoi()" o " strtoll()" è completamente e assolutamente inutile perché non ti danno alcuna indicazione di ciò che era sbagliato nell'input (e usano una definizione completamente irrilevante e inappropriata di ciò che è / non è "valido" ingresso").

Invece, cominciamo a scrivere qualcosa che non è inutile:

char *convertStringToInteger(int *outValue, char *string, int minValue, int maxValue) {
    return "Code not implemented yet!";
}

int main(int argc, char *argv[]) {
    char *errorString;
    int value;

    if(argc < 2) {
        printf("ERROR: No command line argument.\n");
        return EXIT_FAILURE;
    }
    errorString = convertStringToInteger(&value, argv[1], -10, 2000);
    if(errorString != NULL) {
        printf("ERROR: %s\n", errorString);
        return EXIT_FAILURE;
    }
    printf("SUCCESS: Your number is %d\n", value);
    return EXIT_SUCCESS;
}

Per soddisfare i requisiti dichiarati; convertStringToInteger()è probabile che questa funzione finisca per essere diverse centinaia di righe di codice da sola.

Ora, questo era solo "analizzare un singolo intero decimale semplice". Immagina se volessi analizzare qualcosa di complesso; come un elenco di strutture "nome, indirizzo, numero di telefono, indirizzo e-mail"; o forse come un linguaggio di programmazione. In questi casi potrebbe essere necessario scrivere migliaia di righe di codice per creare un analisi che non sia uno scherzo paralizzato.

In altre parole...

Cosa posso usare per analizzare l'input invece di scanf?

Scrivi (potenzialmente migliaia di righe) di codice tu stesso, per soddisfare le tue esigenze.


5

Ecco un esempio dell'uso flexdella scansione di un semplice input, in questo caso un file di numeri in virgola mobile ASCII che potrebbe essere nei formati US ( n,nnn.dd) o European ( n.nnn,dd). Questo è appena copiato da un programma molto più grande, quindi potrebbero esserci dei riferimenti irrisolti:

/* This scanner reads a file of numbers, expecting one number per line.  It  */
/* allows for the use of European-style comma as decimal point.              */

%{
  #include <stdlib.h>
  #include <stdio.h>
  #include <string.h>
  #ifdef WINDOWS
    #include <io.h>
  #endif
  #include "Point.h"

  #define YY_NO_UNPUT
  #define YY_DECL int f_lex (double *val)

  double atofEuro (char *);
%}

%option prefix="f_"
%option nounput
%option noinput

EURONUM [-+]?[0-9]*[,]?[0-9]+([eE][+-]?[0-9]+)?
NUMBER  [-+]?[0-9]*[\.]?[0-9]+([eE][+-]?[0-9]+)?
WS      [ \t\x0d]

%%

[!@#%&*/].*\n

^{WS}*{EURONUM}{WS}*  { *val = atofEuro (yytext); return (1); }
^{WS}*{NUMBER}{WS}*   { *val = atof (yytext); return (1); }

[\n]
.


%%

/*------------------------------------------------------------------------*/

int scan_f (FILE *in, double *vals, int max)
{
  double *val;
  int npts, rc;

  f_in = in;
  val  = vals;
  npts = 0;
  while (npts < max)
  {
    rc = f_lex (val);

    if (rc == 0)
      break;
    npts++;
    val++;
  }

  return (npts);
}

/*------------------------------------------------------------------------*/

int f_wrap ()
{
  return (1);
}

-5

Altre risposte forniscono i giusti dettagli di basso livello, quindi mi limiterò a un livello superiore: in primo luogo, analizza l'aspetto che ti aspetti da ogni linea di input. Prova a descrivere l'input con una sintassi formale - per fortuna, scoprirai che può essere descritto usando una grammatica regolare o almeno una grammatica senza contesto . Se è sufficiente una grammatica regolare, è possibile codificare una macchina a stati finitiche riconosce e interpreta ogni riga di comando un carattere alla volta. Il codice leggerà quindi una riga (come spiegato in altre risposte), quindi eseguirà la scansione dei caratteri nel buffer attraverso la macchina a stati. In alcuni stati, ti fermi e converti la sottostringa scansionata finora in un numero o altro. Probabilmente puoi "tirare il tuo" se è così semplice; se trovate avete bisogno di una grammatica completa context-free si sta meglio capire come utilizzare gli strumenti esistenti parsing (re: lexe yacco le loro varianti).


Una macchina a stati finiti può essere eccessiva; sono possibili modi più semplici per rilevare l'overflow nelle conversioni (come verificare se errno == EOVERFLOWdopo l'uso strtoll).
SS Anne,

1
Perché dovresti codificare la tua macchina a stati finiti, quando flex rende la loro scrittura banalmente semplice?
jamesqf,
Utilizzando il nostro sito, riconosci di aver letto e compreso le nostre Informativa sui cookie e Informativa sulla privacy.
Licensed under cc by-sa 3.0 with attribution required.