O que posso usar para conversão de entrada em vez de scanf?

125

Vi muitas vezes pessoas desencorajando outras a usarem scanfe dizendo que existem alternativas melhores. No entanto, tudo o que acabo vendo é "não use scanf" ou "aqui está uma string de formato correta" , e nunca nenhum exemplo das "alternativas melhores" mencionadas.

Por exemplo, vamos pegar este trecho de código:

scanf("%c", &c);

Isso lê o espaço em branco que foi deixado no fluxo de entrada após a última conversão. A solução sugerida usual para isso é usar:

scanf(" %c", &c);

ou para não usar scanf.

Como scanfé ruim, quais são algumas das opções do ANSI C para converter formatos de entrada que scanfgeralmente podem manipular (como números inteiros, números de ponto flutuante e seqüências de caracteres) sem usar scanf?

SS Anne
fonte

Respostas:

87

As maneiras mais comuns de ler entradas são:

  • usando fgetscom um tamanho fixo, que é geralmente sugerido e

  • usando fgetc, o que pode ser útil se você estiver lendo apenas um char.

Para converter a entrada, há uma variedade de funções que você pode usar:

  • strtoll, para converter uma sequência em um número inteiro

  • strtof/ d/ ld, para converter uma string em um número de ponto flutuante

  • sscanf, que não é tão ruim quanto simplesmente usar scanf, embora tenha a maioria das quedas mencionadas abaixo

  • Não há boas maneiras de analisar uma entrada separada por delimitador em ANSI C. simples. Use strtok_rdo POSIX ou strtok, que não é seguro para threads. Você também pode rolar sua própria variante segura de thread usando strcspne strspn, como strtok_rnão envolve nenhum suporte especial ao sistema operacional.

  • Pode ser um exagero, mas você pode usar lexers e analisadores ( flexe bisonsendo os exemplos mais comuns).

  • Sem conversão, basta usar a string


Como eu não entrei exatamente por que scanf é ruim na minha pergunta, vou elaborar:

  • Com os especificadores de conversão %[...]e %c, scanfnão consome espaço em branco. Aparentemente, isso não é amplamente conhecido, como evidenciado pelas muitas duplicatas dessa questão .

  • Há alguma confusão sobre quando usar o &operador unário ao se referir aos scanfargumentos de (especificamente com strings).

  • É muito fácil ignorar o valor de retorno scanf. Isso poderia facilmente causar comportamento indefinido ao ler uma variável não inicializada.

  • É muito fácil esquecer para evitar o estouro de buffer scanf. scanf("%s", str)é tão ruim quanto, se não pior do que gets,.

  • Você não pode detectar estouro ao converter números inteiros com scanf. De fato, o excesso causa um comportamento indefinido nessas funções.


SS Anne
fonte
56

Por que é scanfruim?

O principal problema é que scanfnunca houve a intenção de lidar com a entrada do usuário. Ele deve ser usado com dados formatados "perfeitamente". Eu citei a palavra "perfeitamente" porque não é completamente verdadeira. Mas não foi projetado para analisar dados que não são confiáveis ​​como a entrada do usuário. Por natureza, a entrada do usuário não é previsível. Os usuários não entendem as instruções, fazem erros de digitação, pressionam acidentalmente a tecla enter antes que terminem etc. Pode-se perguntar, razoavelmente, por que uma função que não deve ser usada para a entrada do usuário é lida stdin. Se você é um usuário experiente * nix, a explicação não será uma surpresa, mas poderá confundir os usuários do Windows. Nos sistemas * nix, é muito comum criar programas que funcionem via canalização,stdoutstdindo segundo. Dessa forma, você pode garantir que a saída e a entrada sejam previsíveis. Durante essas circunstâncias, scanfrealmente funciona bem. Mas, ao trabalhar com informações imprevisíveis, você corre o risco de todos os tipos de problemas.

Então, por que não existem funções padrão fáceis de usar para a entrada do usuário? Só se pode adivinhar aqui, mas suponho que os antigos hackers hardcore de C simplesmente pensassem que as funções existentes eram boas o suficiente, mesmo sendo muito desajeitadas. Além disso, quando você olha para aplicativos de terminal típicos, eles raramente lêem a entrada do usuário stdin. Na maioria das vezes, você passa toda a entrada do usuário como argumentos de linha de comando. Claro, existem exceções, mas para a maioria dos aplicativos, a entrada do usuário é muito pequena.

Então o que você pode fazer?

O meu favorito é fgetsem combinação com sscanf. Certa vez, escrevi uma resposta sobre isso, mas vou postar novamente o código completo. Aqui está um exemplo com verificação e análise de erro decente (mas não perfeita). É bom o suficiente para fins de depuração.

Nota

Não gosto particularmente de pedir ao usuário para inserir duas coisas diferentes em uma única linha. Eu só faço isso quando eles pertencem um ao outro de maneira natural. Como por exemplo printf("Enter the price in the format <dollars>.<cent>: ")e depois use sscanf(buffer "%d.%d", &dollar, &cent). Eu nunca faria algo assim printf("Enter height and base of the triangle: "). O ponto principal do uso fgetsabaixo é encapsular as entradas para garantir que uma entrada não afete a próxima.

#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 você fizer muitas dessas, recomendo criar um wrapper que sempre libere:

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;
}```

Fazer isso eliminará um problema comum, que é a nova linha à direita que pode interferir na entrada do nest. Mas tem outro problema, que é se a linha for maior que bsize. Você pode verificar isso com if(buffer[strlen(buffer)-1] != '\n'). Se você deseja remover a nova linha, faça isso com buffer[strcspn(buffer, "\n")] = 0.

Em geral, aconselho a não esperar que o usuário insira entrada em algum formato estranho que você deve analisar em diferentes variáveis. Se você deseja atribuir as variáveis heighte width, não peça as duas ao mesmo tempo. Permita que o usuário pressione enter entre eles. Além disso, essa abordagem é muito natural em um sentido. Você nunca receberá a entrada stdinaté pressionar Enter, então por que nem sempre lê a linha inteira? Obviamente, isso ainda pode levar a problemas se a linha for maior que o buffer. Lembrei-me de mencionar que a entrada do usuário é desajeitada em C? :)

Para evitar problemas com linhas maiores que o buffer, você pode usar uma função que aloca automaticamente um buffer de tamanho apropriado, você pode usar getline(). A desvantagem é que você precisará obter freeo resultado posteriormente.

Intensificando o jogo

Se você é sério sobre a criação de programas em C com a entrada do usuário, eu recomendaria dar uma olhada em uma biblioteca como ncurses. Porque então você provavelmente também deseja criar aplicativos com alguns gráficos de terminal. Infelizmente, você perderá alguma portabilidade se fizer isso, mas isso oferece um controle muito melhor da entrada do usuário. Por exemplo, permite ler instantaneamente um pressionamento de tecla, em vez de esperar que o usuário pressione enter.

Klutt
fonte
Observe que (r = sscanf("1 2 junk", "%d%d", &x, &y)) != 2não detecta como incorreto o texto não numérico à direita.
chux - Restabelece Monica
11
@chux Corrigido% f% f. O que você quer dizer com o primeiro?
klutt
With fgets()of "1 2 junk", if((r = sscanf(buffer, "%d%d", &x, &y)) != 2) {não relata nada de errado com a entrada, mesmo que tenha "lixo".
chux - Restabelece Monica
@chux Ah, agora eu vejo. Bem, isso foi intencional.
klutt
11
scanfdestina-se a ser usado com dados perfeitamente formatados Mas mesmo isso não é verdade. Além do problema com "lixo eletrônico", como mencionado pelo @chux, também existe o fato de que um formato como o "%d %d %d"prazer de ler entradas de uma, duas ou três linhas (ou mais, se houver linhas em branco), que não há O modo de forçar (digamos) uma entrada de duas linhas fazendo algo como "%d\n%d %d"etc. scanfpode ser apropriado para a entrada de fluxo formatada , mas não é nada bom para nada baseado em linhas.
Steve Summit
18

scanfé incrível quando você sabe que sua opinião é sempre bem-estruturada e bem-comportada. De outra forma...

IMO, aqui estão os maiores problemas com scanf:

  • Risco de estouro de buffer - se você não especificar uma largura de campo para os especificadores %se de %[conversão, corre o risco de um estouro de buffer (tentando ler mais entradas do que o tamanho do buffer para armazenar). Infelizmente, não há uma boa maneira de especificar isso como argumento (como printfacontece com ) - você deve codificá-lo como parte do especificador de conversão ou fazer algumas travessuras de macro.

  • Aceita entradas que devem ser rejeitadas - Se você estiver lendo uma entrada com o %despecificador de conversão e digitar algo do tipo 12w4, esperaria scanf rejeitar essa entrada, mas ela não aceita - ele converte e atribui com êxito 12, deixando w4no fluxo de entrada para estragar a próxima leitura.

Então, o que você deve usar?

Normalmente, recomendo a leitura de todas as entradas interativas como texto usando fgets- ele permite que você especifique um número máximo de caracteres para ler por vez, para evitar facilmente o estouro de buffer:

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

Uma das peculiaridades fgetsé que ela armazenará a nova linha à direita no buffer, se houver espaço, para que você possa fazer uma verificação fácil para ver se alguém digitou mais entradas do que você esperava:

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

Como você lida com isso é com você - você pode rejeitar toda a entrada de imediato e usar toda a entrada restante com getchar:

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

Ou você pode processar a entrada obtida até agora e ler novamente. Depende do problema que você está tentando resolver.

Para tokenizar a entrada (dividi-la com base em um ou mais delimitadores), você pode usar strtok, mas cuidado - strtokmodifica sua entrada (substitui os delimitadores pelo terminador de strings) e não pode preservar seu estado (por exemplo, você pode ' t tokenize parcialmente uma sequência, depois comece a tokenizar outra e, em seguida, continue de onde parou na sequência original). Existe uma variante, strtok_sque preserva o estado do tokenizer, mas a implementação do AFAIK é opcional (você precisará verificar se __STDC_LIB_EXT1__está definido para ver se está disponível).

Depois de tokenizar sua entrada, se você precisar converter seqüências de caracteres em números (por exemplo, "1234"=> 1234), você terá opções. strtole strtodconverterá representações de seqüência de caracteres de números inteiros e reais em seus respectivos tipos. Eles também permitem que você pegue o 12w4problema mencionado acima - um dos argumentos deles é um ponteiro para o primeiro caractere não convertido na string:

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;
John Bode
fonte
Se você não especificar uma largura de campo ... - ou uma supressão de conversão (por exemplo %*[%\n], útil para lidar com linhas longas mais adiante na resposta).
precisa
Existe uma maneira de obter especificações em tempo de execução das larguras dos campos, mas isso não é legal. Você acaba tendo que construir a string de formato no seu código (talvez usando snprintf()),.
precisa
5
Você cometeu o erro mais comum isspace()lá - ele aceita caracteres não assinados representados como int, então você precisa converter unsigned charpara evitar o UB nas plataformas onde charestá assinado.
precisa
9

Nesta resposta, vou assumir que você está lendo e interpretando linhas de texto . Talvez você esteja solicitando ao usuário, que está digitando alguma coisa e pressionando RETURN. Ou talvez você esteja lendo linhas de texto estruturado de algum tipo de arquivo de dados.

Como você está lendo linhas de texto, faz sentido organizar seu código em torno de uma função de biblioteca que lê, bem, uma linha de texto. A função Padrão é fgets(), embora existam outras (inclusive getline). E então o próximo passo é interpretar essa linha de texto de alguma forma.

Aqui está a receita básica para ligar fgetspara ler uma linha de texto:

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

Isso simplesmente lê uma linha de texto e a imprime novamente. Como está escrito, ele tem algumas limitações, as quais abordaremos em um minuto. Ele também possui um recurso muito bom: esse número 512 que passamos como segundo argumento fgetsé o tamanho da matriz lineque estamos pedindo fgetspara ler. Esse fato - que podemos dizer fgetsquanto é permitido ler - significa que podemos ter certeza de que fgetsnão excederá o array lendo demais nele.

Agora, agora, sabemos ler uma linha de texto, mas e se realmente quisermos ler um número inteiro, um número de ponto flutuante, um único caractere ou uma única palavra? (Isto é, que se o scanfapelo que estamos tentando melhorar estava usando um especificador de formato como %d, %f, %c, ou %s?)

É fácil reinterpretar uma linha de texto - uma string - como qualquer uma dessas coisas. Para converter uma string em um número inteiro, a maneira mais simples (embora imperfeita) de fazer isso é chamar atoi(). Para converter para um número de ponto flutuante, existe atof(). (E também existem maneiras melhores, como veremos em um minuto.) Aqui está um exemplo muito simples:

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 você quiser que o usuário digite um único caractere (talvez you ncomo resposta sim / não), você pode literalmente apenas pegar o primeiro caractere da linha, assim:

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

(Isso ignora, é claro, a possibilidade de o usuário digitar uma resposta com vários caracteres; ignora silenciosamente quaisquer caracteres extras que foram digitados.)

Por fim, se você deseja que o usuário digite uma string definitivamente não contendo espaço em branco, se você deseja tratar a linha de entrada

hello world!

como a sequência "hello"seguida por outra coisa (que é o que o scanfformato %steria feito), nesse caso, eu me enganei um pouco, não é tão fácil reinterpretar a linha dessa maneira, afinal, então a resposta para isso parte da pergunta terá que esperar um pouco.

Mas primeiro quero voltar para três coisas que pulei.

(1) Temos chamado

fgets(line, 512, stdin);

para ler na matriz linee onde 512 é o tamanho da matriz linepara fgetsque não a transborde. Mas, para garantir que 512 seja o número certo (especialmente, para verificar se alguém alterou o programa para alterar o tamanho), você deve ler novamente onde quer que tenha linesido declarado. Isso é um incômodo, então existem duas maneiras muito melhores de manter os tamanhos sincronizados. Você poderia, (a) usar o pré-processador para criar um nome para o tamanho:

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

Ou, (b) use o sizeofoperador de C :

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

(2) O segundo problema é que não temos verificado erros. Ao ler a entrada, você deve sempre verificar a possibilidade de erro. Se, por qualquer motivo, fgetsnão puder ler a linha de texto solicitada, isso indica o retorno de um ponteiro nulo. Então deveríamos estar fazendo coisas como

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

Finalmente, há o problema de que, para ler uma linha de texto, fgetslê os caracteres e os preenche em sua matriz até encontrar o \ncaractere que termina a linha e preenche o \ncaractere também em sua matriz . Você pode ver isso se modificar um pouco o exemplo anterior:

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

Se eu executar isso e digitar "Steve" quando solicitado, ele será impresso

you typed: "Steve
"

Isso "na segunda linha é porque a string que ele leu e imprimiu foi realmente "Steve\n".

Às vezes, essa nova linha extra não importa (como quando ligamos atoiou atof, pois ambos ignoram qualquer entrada não numérica extra após o número), mas às vezes isso importa muito. Muitas vezes, vamos querer retirar essa nova linha. Existem várias maneiras de fazer isso, que abordarei em um minuto. (Eu sei que tenho falado muito disso. Mas voltarei a todas essas coisas, prometo.)

Nesse ponto, você deve estar pensando: "Pensei que você dissesse que scanf não era bom, e que esse outro caminho seria muito melhor. Mas fgetsestá começando a parecer um incômodo. Ligar scanfera tão fácil ! Não posso continuar usando?" "

Claro, você pode continuar usando scanf, se quiser. (E, para coisas realmente simples, de certa forma, é mais simples.) Mas, por favor, não venha chorar quando você falhar por causa de uma de suas 17 peculiaridades e fraquezas, ou entrar em um loop infinito por causa da entrada de seu não esperava, ou quando você não consegue descobrir como usá-lo para fazer algo mais complicado. E vamos dar uma olhada nos fgetsincômodos reais:

  1. Você sempre precisa especificar o tamanho da matriz. Bem, é claro, isso não é um incômodo - é um recurso, porque o estouro de buffer é uma coisa realmente ruim.

  2. Você precisa verificar o valor de retorno. Na verdade, isso é uma lavagem, porque para usar scanfcorretamente, você também deve verificar o valor de retorno.

  3. Você tem que tirar as \ncostas. Isso é, admito, um verdadeiro incômodo. Eu gostaria que houvesse uma função padrão que eu pudesse apontar para você que não tivesse esse pequeno problema. (Por favor, ninguém trate de falar gets.) Mas comparado a scanf's17 diferentes incômodos, eu o levarei a fgetsqualquer dia.

Então, como é que você tira essa nova linha? Três caminhos:

a) Maneira óbvia:

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

(b) Maneira complicada e compacta:

strtok(line, "\n");

Infelizmente, este nem sempre funciona.

(c) Outra maneira compacta e levemente obscura:

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

E agora que isso está fora do caminho, podemos voltar para outra coisa que eu pulei: as imperfeições de atoi()e atof(). O problema é que eles não fornecem nenhuma indicação útil de sucesso de sucesso ou fracasso: ignoram silenciosamente as entradas não numéricas à direita e retornam 0 silenciosamente, se não houver nenhuma entrada numérica. As alternativas preferidas - que também têm outras vantagens - são strtole strtod. strtoltambém permite usar uma base diferente de 10, o que significa que você pode obter o efeito de (entre outras coisas) %oou %xcomscanf. Mas mostrar como usar essas funções corretamente é uma história em si, e seria uma distração demais para o que já está se transformando em uma narrativa bastante fragmentada, então não vou dizer mais nada sobre elas agora.

O restante da narrativa principal diz respeito à entrada que você pode estar tentando analisar que é mais complicado do que apenas um único número ou caractere. E se você quiser ler uma linha que contém dois números, ou várias palavras separadas por espaços em branco, ou pontuação de estrutura específica? É aí que as coisas ficam interessantes e onde as coisas provavelmente estavam ficando complicadas se você estivesse tentando fazer as coisas usando scanf, e onde há muito mais opções agora que você leu uma linha de texto de maneira limpa fgets, embora a história completa de todas essas opções provavelmente poderia encher um livro, então só poderemos arranhar a superfície aqui.

  1. Minha técnica favorita é dividir a linha em "palavras" separadas por espaços em branco e fazer algo mais a cada "palavra". Uma função padrão principal para fazer isso é strtok(que também tem seus problemas e que também classifica toda uma discussão separada). Minha preferência é uma função dedicada à construção de uma matriz de ponteiros para cada "palavra" desmembrada, uma função que descrevo nestas notas do curso . De qualquer forma, uma vez que você tenha "palavras", poderá processar cada uma delas, talvez com as mesmas atoi/ atof/ strtol/ strtod funções que já examinamos.

  2. Paradoxalmente, mesmo que tenhamos gasto scanfbastante tempo e esforço aqui para descobrir como nos afastar , outra boa maneira de lidar com a linha de texto com a qual acabamos de ler fgetsé passar para ela sscanf. Dessa forma, você acaba com a maioria das vantagens de scanf, mas sem a maioria das desvantagens.

  3. Se sua sintaxe de entrada for particularmente complicada, pode ser apropriado usar uma biblioteca "regexp" para analisá-la.

  4. Finalmente, você pode usar as soluções de análise ad hoc que mais lhe convierem. Você pode mover através da linha um caractere de cada vez com um char *ponteiro verificando os caracteres esperados. Ou você pode procurar caracteres específicos usando funções como strchrou strrchr, ou strspnou strcspn, ou strpbrk. Ou você pode analisar / converter e pular grupos de caracteres de dígitos usando as funções strtolou strtodque ignoramos anteriormente.

Obviamente, há muito mais a ser dito, mas espero que esta introdução o inicie.

Steve Summit
fonte
Existe uma boa razão para escrever, em sizeof (line)vez de simplesmente sizeof line? O primeiro faz parecer que lineé um nome de tipo!
precisa
@TobySpeight Um bom motivo? Não, duvido. Os parênteses são meu hábito, porque não consigo me preocupar em lembrar se são objetos ou nomes de tipos para os quais são necessários, mas muitos programadores os deixam de fora quando podem. (Para mim, é uma questão de preferência pessoal e estilo, e um menor bem nisso.)
Steve Summit
+1 para usar sscanfcomo um mecanismo de conversão, mas para coletar (e possivelmente massagear) a entrada com uma ferramenta diferente. Mas talvez valha a pena mencionar getlinenesse contexto.
dmckee --- ex-moderador gatinho
Quando você fala dos " fscanfincômodos reais", você quer dizer fgets? E o incômodo n ° 3 realmente me incomoda, especialmente porque scanfretorna um ponteiro inútil para o buffer em vez de retornar o número de caracteres introduzidos (o que tornaria a remoção da nova linha muito mais limpa).
Supercat
11
Obrigado pela explicação do seu sizeofestilo. Para mim, é fácil lembrar quando você precisa dos parênteses: penso (type)que é como um elenco sem valor (porque estamos interessados ​​apenas no tipo). Outra coisa: você diz que strtok(line, "\n")nem sempre funciona, mas não é óbvio quando não pode. Acho que você está pensando no caso em que a linha era maior que o buffer, então não temos nova linha e strtok()retorna nulo? É uma pena fgets()que não retorne um valor mais útil para que possamos saber se a nova linha está lá ou não.
precisa
7

O que posso usar para analisar a entrada em vez do scanf?

Em vez de scanf(some_format, ...), considere fgets()comsscanf(buffer, some_format_and %n, ...)

Ao usar " %n", o código pode simplesmente detectar se todo o formato foi verificado com êxito e se não havia lixo extra de espaço em branco no final.

// 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.
  }
chux - Restabelecer Monica
fonte
6

Vamos declarar os requisitos de análise como:

  • entrada válida deve ser aceita (e convertida em alguma outra forma)

  • entrada inválida deve ser rejeitada

  • quando qualquer entrada é rejeitada, é necessário fornecer ao usuário uma mensagem descritiva que explique (em linguagem clara "facilmente compreendida por pessoas normais que não são programadores") por que ela foi rejeitada (para que as pessoas possam descobrir como corrigir o problema problema)

Para manter as coisas muito simples, vamos considerar a análise de um único número inteiro decimal simples (digitado pelo usuário) e nada mais. Os possíveis motivos para a entrada do usuário ser rejeitada são:

  • a entrada continha caracteres inaceitáveis
  • a entrada representa um número menor que o mínimo aceito
  • a entrada representa um número maior que o máximo aceito
  • a entrada representa um número que possui uma parte fracionária diferente de zero

Vamos definir também "a entrada continha caracteres inaceitáveis" corretamente; e diga o seguinte:

  • os espaços em branco à esquerda e os à direita serão ignorados (por exemplo, "
    5" será tratado como "5")
  • é permitido zero ou um ponto decimal (por exemplo, "1234" e "1234.000" são tratados da mesma forma que "1234")
  • deve haver pelo menos um dígito (por exemplo, "." é rejeitado)
  • não é permitido mais de um ponto decimal (por exemplo, "1.2.3" é rejeitado)
  • vírgulas que não estejam entre dígitos serão rejeitadas (por exemplo, ", 1234" é rejeitado)
  • vírgulas após um ponto decimal serão rejeitadas (por exemplo, "1234.000.000" é rejeitado)
  • vírgulas que são depois de outra vírgula são rejeitadas (por exemplo, "1, 234" é rejeitado)
  • todas as outras vírgulas serão ignoradas (por exemplo, "1.234" será tratada como "1234")
  • um sinal de menos que não é o primeiro caractere que não é um espaço em branco é rejeitado
  • um sinal positivo que não é o primeiro caractere que não é um espaço em branco é rejeitado

A partir disso, podemos determinar que as seguintes mensagens de erro são necessárias:

  • "Caractere desconhecido no início da entrada"
  • "Caractere desconhecido no final da entrada"
  • "Caractere desconhecido no meio da entrada"
  • "O número é muito baixo (o mínimo é ....)"
  • "O número é muito alto (o máximo é ....)"
  • "Número não é um número inteiro"
  • "Muitos pontos decimais"
  • "Sem dígitos decimais"
  • "Vírgula incorreta no início do número"
  • "Vírgula incorreta no final do número"
  • "Vírgula incorreta no meio do número"
  • "Vírgula inválida após ponto decimal"

A partir deste ponto, podemos ver que uma função adequada para converter uma string em um número inteiro precisaria distinguir entre tipos muito diferentes de erros; e que algo como " scanf()" ou " atoi()" ou " strtoll()" é completamente inútil, porque eles não dão nenhuma indicação do que estava errado com a entrada (e usam uma definição completamente irrelevante e inadequada do que é / não é "válido entrada").

Em vez disso, vamos começar a escrever algo que não é inútil:

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;
}

Para atender aos requisitos estabelecidos; convertStringToInteger()é provável que essa função acabe sendo várias centenas de linhas de código por si só.

Agora, isso era apenas "analisando um único número decimal simples". Imagine se você quisesse analisar algo complexo; como uma lista de estruturas "nome, endereço, número de telefone, endereço de email"; ou talvez como uma linguagem de programação. Para esses casos, pode ser necessário escrever milhares de linhas de código para criar uma análise que não seja uma piada aleijada.

Em outras palavras...

O que posso usar para analisar a entrada em vez do scanf?

Escreva você mesmo (potencialmente milhares de linhas) de código para atender às suas necessidades.

Brendan
fonte
5

Aqui está um exemplo de flexcomo digitalizar uma entrada simples, neste caso, um arquivo de números de ponto flutuante ASCII que pode estar nos formatos US ( n,nnn.dd) ou European ( n.nnn,dd). Isso é apenas copiado de um programa muito maior, portanto, pode haver algumas referências não resolvidas:

/* 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);
}
jamesqf
fonte
-5

Outras respostas fornecem os detalhes corretos de baixo nível, então vou me limitar a um nível superior: primeiro, analise como você espera que cada linha de entrada seja. Tente descrever a entrada com uma sintaxe formal - com sorte, você descobrirá que ela pode ser descrita usando uma gramática regular ou pelo menos uma gramática livre de contexto . Se uma gramática regular for suficiente, você poderá codificar uma máquina de estado finitoque reconhece e interpreta cada linha de comando, um caractere de cada vez. Seu código lerá uma linha (como explicado em outras respostas) e, em seguida, varrerá os caracteres no buffer pela máquina de estado. Em certos estados, você para e converte a substring digitalizada até agora em um número ou qualquer outra coisa. Provavelmente você pode 'rolar sozinho' se for simples assim; se você precisar de uma gramática livre de contexto, é melhor descobrir como usar as ferramentas de análise existentes (re: lexe yaccou suas variantes).

PMar
fonte
Uma máquina de estado finito pode ser um exagero; maneiras mais fáceis de detectar estouro nas conversões (como verificar se errno == EOVERFLOWapós o uso strtoll) são possíveis.
SS Anne
11
Por que você codificaria sua própria máquina de estados finitos, quando o flex torna sua redação trivialmente simples?
jamesqf