Por que é scanf
ruim?
O principal problema é que scanf
nunca 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,stdout
stdin
do segundo. Dessa forma, você pode garantir que a saída e a entrada sejam previsíveis. Durante essas circunstâncias, scanf
realmente 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 é fgets
em 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, ¢)
. Eu nunca faria algo assim printf("Enter height and base of the triangle: ")
. O ponto principal do uso fgets
abaixo é 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 height
e 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 stdin
até 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 free
o 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.
(r = sscanf("1 2 junk", "%d%d", &x, &y)) != 2
não detecta como incorreto o texto não numérico à direita.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".scanf
destina-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.scanf
pode ser apropriado para a entrada de fluxo formatada , mas não é nada bom para nada baseado em linhas.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
%s
e 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 (comoprintf
acontece 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
%d
especificador de conversão e digitar algo do tipo12w4
, esperariascanf
rejeitar essa entrada, mas ela não aceita - ele converte e atribui com êxito12
, deixandow4
no 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: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:Como você lida com isso é com você - você pode rejeitar toda a entrada de imediato e usar toda a entrada restante com
getchar
: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 -strtok
modifica 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_s
que 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.strtol
estrtod
converterá representações de seqüência de caracteres de números inteiros e reais em seus respectivos tipos. Eles também permitem que você pegue o12w4
problema mencionado acima - um dos argumentos deles é um ponteiro para o primeiro caractere não convertido na string:fonte
%*[%\n]
, útil para lidar com linhas longas mais adiante na resposta).snprintf()
),.isspace()
lá - ele aceita caracteres não assinados representados comoint
, então você precisa converterunsigned char
para evitar o UB nas plataformas ondechar
está assinado.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 (inclusivegetline
). E então o próximo passo é interpretar essa linha de texto de alguma forma.Aqui está a receita básica para ligar
fgets
para ler uma linha de texto: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 matrizline
que estamos pedindofgets
para ler. Esse fato - que podemos dizerfgets
quanto é permitido ler - significa que podemos ter certeza de quefgets
nã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
scanf
apelo 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, existeatof()
. (E também existem maneiras melhores, como veremos em um minuto.) Aqui está um exemplo muito simples:Se você quiser que o usuário digite um único caractere (talvez
y
oun
como resposta sim / não), você pode literalmente apenas pegar o primeiro caractere da linha, assim:(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
como a sequência
"hello"
seguida por outra coisa (que é o que oscanf
formato%s
teria 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
para ler na matriz
line
e onde 512 é o tamanho da matrizline
parafgets
que 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 tenhaline
sido 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:Ou, (b) use o
sizeof
operador de C :(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,
fgets
não puder ler a linha de texto solicitada, isso indica o retorno de um ponteiro nulo. Então deveríamos estar fazendo coisas comoFinalmente, há o problema de que, para ler uma linha de texto,
fgets
lê os caracteres e os preenche em sua matriz até encontrar o\n
caractere que termina a linha e preenche o\n
caractere também em sua matriz . Você pode ver isso se modificar um pouco o exemplo anterior:Se eu executar isso e digitar "Steve" quando solicitado, ele será impresso
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
atoi
ouatof
, 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. Masfgets
está começando a parecer um incômodo. Ligarscanf
era 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 nosfgets
incômodos reais: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.
Você precisa verificar o valor de retorno. Na verdade, isso é uma lavagem, porque para usar
scanf
corretamente, você também deve verificar o valor de retorno.Você tem que tirar as
\n
costas. 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 falargets
.) Mas comparado ascanf's
17 diferentes incômodos, eu o levarei afgets
qualquer dia.Então, como é que você tira essa nova linha? Três caminhos:
a) Maneira óbvia:
(b) Maneira complicada e compacta:
Infelizmente, este nem sempre funciona.
(c) Outra maneira compacta e levemente obscura:
E agora que isso está fora do caminho, podemos voltar para outra coisa que eu pulei: as imperfeições de
atoi()
eatof()
. 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ãostrtol
estrtod
.strtol
também permite usar uma base diferente de 10, o que significa que você pode obter o efeito de (entre outras coisas)%o
ou%x
comscanf
. 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 limpafgets
, embora a história completa de todas essas opções provavelmente poderia encher um livro, então só poderemos arranhar a superfície aqui.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 mesmasatoi
/atof
/strtol
/strtod
funções que já examinamos.Paradoxalmente, mesmo que tenhamos gasto
scanf
bastante 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 lerfgets
é passar para elasscanf
. Dessa forma, você acaba com a maioria das vantagens descanf
, mas sem a maioria das desvantagens.Se sua sintaxe de entrada for particularmente complicada, pode ser apropriado usar uma biblioteca "regexp" para analisá-la.
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 comostrchr
oustrrchr
, oustrspn
oustrcspn
, oustrpbrk
. Ou você pode analisar / converter e pular grupos de caracteres de dígitos usando as funçõesstrtol
oustrtod
que ignoramos anteriormente.Obviamente, há muito mais a ser dito, mas espero que esta introdução o inicie.
fonte
sizeof (line)
vez de simplesmentesizeof line
? O primeiro faz parecer queline
é um nome de tipo!sscanf
como um mecanismo de conversão, mas para coletar (e possivelmente massagear) a entrada com uma ferramenta diferente. Mas talvez valha a pena mencionargetline
nesse contexto.fscanf
incômodos reais", você quer dizerfgets
? E o incômodo n ° 3 realmente me incomoda, especialmente porquescanf
retorna 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).sizeof
estilo. 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 questrtok(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 estrtok()
retorna nulo? É uma penafgets()
que não retorne um valor mais útil para que possamos saber se a nova linha está lá ou não.Em vez de
scanf(some_format, ...)
, considerefgets()
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.fonte
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:
Vamos definir também "a entrada continha caracteres inaceitáveis" corretamente; e diga o seguinte:
5" será tratado como "5")
A partir disso, podemos determinar que as seguintes mensagens de erro são necessárias:
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:
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...
Escreva você mesmo (potencialmente milhares de linhas) de código para atender às suas necessidades.
fonte
Aqui está um exemplo de
flex
como 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:fonte
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:
lex
eyacc
ou suas variantes).fonte
errno == EOVERFLOW
após o usostrtoll
) são possíveis.