C Validar a Entrada
Quem usa o seu programa erra: digita seis em vez de 6, aperta Enter sem escrever nada, põe uma nota 15 numa escala de 0 a 10, cola um texto enorme. Um programa bem-feito não trava, não entra em laço infinito e não segue em frente com um valor errado: ele confere o que chegou e, se não serve, explica o problema e pergunta de novo. Nesta aula você vai juntar o que aprendeu em Ler uma Linha e Texto ↔ Número numa função que pede um número até ele ser válido, e que você vai reaproveitar em todos os seus programas.
- Os jeitos como uma entrada pode chegar errada, e o que o programa deve fazer em cada um.
- Como pedir de novo com
scanf: conferir o retorno e jogar fora o resto da linha. - O jeito robusto: ler a linha com
fgets, converter comstrtole conferir a faixa. - Uma função
ler_intpara reutilizar, que também trata o fim da entrada (Ctrl+D). - O que fazer quando a linha é maior que o array.
- Como validar texto: nome em branco e resposta
s/n.
Você vai precisar de Ler uma Linha, Texto ↔ Número e Funções. Todos os programas foram compilados com o gcc 15.2 na máquina fictícia da Ana (usuária ana, computador notebook), na pasta ~/c, com gcc -std=c23 -Wall -Wextra arquivo.c -o programa. Nos terminais, o que vem depois da pergunta (como oito em Nota (0 a 10): oito) foi o que a Ana digitou.
A ideia: o caixa que confere a nota
No caixa da padaria, a atendente não aceita qualquer papel como dinheiro. Ela olha a nota: é dinheiro de verdade? É o valor certo? Se tem algo errado, ela devolve e diz o motivo: "essa nota está rasgada, tem outra?". E não fecha a venda até receber um pagamento que sirva.
Validar a entrada é isso: o programa olha o que a pessoa digitou antes de usar, devolve o que não serve com uma explicação e pede de novo. A regra de ouro: nunca confie na entrada. Nem na sua: você mesma vai errar ao testar.
1. O que pode chegar errado
Este programa da lojinha da Ana confia no que recebe:
#include <stdio.h>
int main(void) {
int qtd = 0;
printf("Quantos pães? ");
scanf("%d", &qtd);
printf("Total: R$ %.2f\n", qtd * 0.75);
return 0;
}
$ gcc -std=c23 -Wall -Wextra confia.c -o confia $ ./confia Quantos pães? 6 Total: R$ 4.50 $ ./confia Quantos pães? seis Total: R$ 0.00 « não leu nada e seguiu com 0 $ ./confia Quantos pães? -3 Total: R$ -2.25 « um número, mas sem sentido $ ./confia Quantos pães? 6abc Total: R$ 4.50 « aceitou o 6 e ignorou o resto, calado
O compilador não reclamou de nada, e o programa nunca avisou a Ana. Os problemas possíveis são sempre estes:
| A pessoa… | Exemplo | O programa deve… |
|---|---|---|
| digitou texto no lugar de número | seis | dizer que não é número e pedir de novo |
| apertou Enter sem escrever nada | (linha vazia) | pedir de novo |
| escreveu um número com sobra | 6abc, 7.5, 20 anos | recusar a linha inteira |
| escreveu um número fora da faixa | -3, 15 numa nota de 0 a 10 | dizer qual é a faixa e pedir de novo |
| escreveu um número enorme | 99999999999999999999 | tratar como fora da faixa |
| colou um texto maior que o array | 200 caracteres | jogar fora o excesso (seção 5) |
| encerrou a entrada | Ctrl+D | parar de perguntar e encerrar com uma mensagem |
O último caso é o mais esquecido: quando a entrada acaba, não adianta perguntar de novo, ninguém vai responder. Um programa que insiste entra num laço infinito (você vai ver isso em Quando dá errado).
2. Pedir de novo com scanf: jogar fora o resto da linha
Na aula while você viu que um scanf("%d") que falha deixa o texto parado na entrada: a próxima tentativa tropeça nele de novo, e de novo, para sempre. Por isso, em do while, o programa das notas encerrava ao receber uma letra. Para pedir de novo, falta uma peça: jogar fora o resto da linha depois de cada leitura, dando certo ou não.
#include <stdio.h>
int main(void) {
int nota = 0;
while (true) {
printf("Nota (0 a 10): ");
int lidos = scanf("%d", ¬a);
if (lidos == EOF) {
printf("\nA entrada acabou.\n");
return 1;
}
int c;
while ((c = getchar()) != '\n' && c != EOF) {
// joga fora o resto da linha
}
if (lidos != 1) {
printf("Isso não é um número. Tente de novo.\n");
} else if (nota < 0 || nota > 10) {
printf("A nota vai de 0 a 10. Tente de novo.\n");
} else {
break;
}
}
printf("Nota registrada: %d\n", nota);
return 0;
}
- while (true)
- Um laço "para sempre": só termina pelo
break(nota válida) ou peloreturn(entrada acabou). Veja break e continue.trueé palavra-chave desde o C23; antes, era preciso#include <stdbool.h>ou escreverwhile (1). - int lidos = scanf("%d", ¬a);
- Guarda o que o
scanfdevolve:1se leu o número,0se encontrou algo que não é número,EOF(um valor negativo) se a entrada acabou antes de ler qualquer coisa. - if (lidos == EOF)
- A entrada acabou (Ctrl+D, ou o fim de um arquivo). O
\nno começo da mensagem é porque o cursor ficou parado depois da pergunta. - while ((c = getchar()) != '\n' && c != EOF)
- O laço de descarte da aula Ler uma Linha: lê um caractere por vez e para no Enter. Ele leva embora o
oitoque travaria oscanf, e também o\nde uma leitura que deu certo. - if (lidos != 1) … else if (nota < 0 || nota > 10) … else break;
- Primeiro "é um número?", depois "está na faixa?". Só com as duas respostas "sim" o laço termina.
$ gcc -std=c23 -Wall -Wextra nota.c -o nota $ ./nota Nota (0 a 10): oito Isso não é um número. Tente de novo. Nota (0 a 10): 15 A nota vai de 0 a 10. Tente de novo. Nota (0 a 10): 8 Nota registrada: 8 $ ./nota Nota (0 a 10): oito Isso não é um número. Tente de novo. Nota (0 a 10): « a Ana apertou Ctrl+D A entrada acabou. $ echo $? 1
Funciona, mas o scanf tem um defeito que nenhum descarte conserta: ele lê o começo da linha e não se importa com o resto.
$ ./nota Nota (0 a 10): 8abc Nota registrada: 8 « o "abc" foi para o lixo sem aviso $ ./nota Nota (0 a 10): 7.5 Nota registrada: 7 « o ".5" também
Para recusar a linha inteira quando ela não é só um número, o jeito é ler a linha toda primeiro e examiná-la depois (seção 3).
Missão: este programa encerra quando recebe uma letra. Faça-o pedir de novo: escreva o laço que joga fora o resto da linha (com getchar) logo depois do if (lidos == EOF) e apague o return 1; do "não é um número". A entrada é oito e depois 8. (Se você só apagar o return 1;, sem o descarte, o programa repete a pergunta sem parar: o editor o interrompe depois de alguns segundos.)
#include <stdio.h>
int main(void) {
int nota = 0;
while (true) {
printf("Nota (0 a 10): ");
int lidos = scanf("%d", ¬a);
if (lidos == EOF) {
printf("\nA entrada acabou.\n");
return 1;
}
if (lidos != 1) {
printf("Isso não é um número. Tente de novo.\n");
return 1;
} else if (nota < 0 || nota > 10) {
printf("A nota vai de 0 a 10. Tente de novo.\n");
} else {
break;
}
}
printf("Nota registrada: %d\n", nota);
return 0;
}
#include <stdio.h>
int main(void) {
int nota = 0;
while (true) {
printf("Nota (0 a 10): ");
int lidos = scanf("%d", ¬a);
if (lidos == EOF) {
printf("\nA entrada acabou.\n");
return 1;
}
int c;
while ((c = getchar()) != '\n' && c != EOF) {
// joga fora o resto da linha
}
if (lidos != 1) {
printf("Isso não é um número. Tente de novo.\n");
} else if (nota < 0 || nota > 10) {
printf("A nota vai de 0 a 10. Tente de novo.\n");
} else {
break;
}
}
printf("Nota registrada: %d\n", nota);
return 0;
}
3. O jeito robusto: fgets + strtol + faixa
A receita que os programadores experientes usam tem três passos, cada um com sua conferência:
- Ler a linha inteira com
fgets. Se ele devolverNULL, a entrada acabou. - Converter com
strtole conferir se a linha era só um número (ofimda aula Texto ↔ Número). - Conferir a faixa: o número faz sentido para o programa?
#include <errno.h>
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
int main(void) {
char linha[100];
long idade = 0;
while (true) {
printf("Idade: ");
if (fgets(linha, sizeof linha, stdin) == NULL) {
printf("\nNenhuma idade digitada.\n");
return 1;
}
linha[strcspn(linha, "\n")] = '\0';
char *fim;
errno = 0;
idade = strtol(linha, &fim, 10);
if (fim == linha || *fim != '\0') {
printf("\"%s\" não é um número inteiro.\n", linha);
} else if (errno == ERANGE || idade < 0 || idade > 130) {
printf("A idade vai de 0 a 130.\n");
} else {
break;
}
}
printf("Idade registrada: %ld\n", idade);
return 0;
}
| Linha | O que faz |
|---|---|
fgets(linha, sizeof linha, stdin) == NULL | Passo 1. Lê a linha inteira, com o Enter. NULL = a entrada acabou: avisa e encerra, sem perguntar de novo. |
linha[strcspn(linha, "\n")] = '\0'; | Tira o Enter do fim. Sem isso, o \n seria a "sobra" depois de todo número. |
errno = 0; | Zera o aviso de erro antes de converter (o strtol só o escreve quando dá erro; veja errno). |
idade = strtol(linha, &fim, 10); | Passo 2. Converte, e fim marca onde a conversão parou. |
fim == linha || *fim != '\0' | Nenhum dígito (vinte, linha vazia) ou sobrou texto (20 anos, 7.5). |
errno == ERANGE || idade < 0 || idade > 130 | Passo 3. Grande demais para um long, ou fora da faixa que faz sentido para uma idade. |
break; | Passou nas três conferências: sai do laço com a idade válida. |
$ gcc -std=c23 -Wall -Wextra idade.c -o idade $ ./idade Idade: vinte "vinte" não é um número inteiro. Idade: « só Enter "" não é um número inteiro. Idade: 20 anos "20 anos" não é um número inteiro. Idade: -5 A idade vai de 0 a 130. Idade: 200 A idade vai de 0 a 130. Idade: 99999999999999999999 A idade vai de 0 a 130. Idade: 29 « espaço antes: o strtol aceita Idade registrada: 29
Cada tipo de erro da tabela da seção 1 recebeu uma resposta, e nenhum deles passou. Repare que a faixa (0 a 130) é uma decisão sua, e não do C: o strtol aceita -5 sem problema, porque -5 é um número perfeitamente bom. Só o programa sabe que uma idade não pode ser negativa.
4. Uma função para reutilizar: ler_int
Num programa de verdade, há várias perguntas: quantidade, dia, mês, nota… Copiar o laço da seção 3 para cada uma seria repetitivo e fácil de errar. A saída é colocar a receita numa função que recebe a pergunta e a faixa:
#include <errno.h>
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
// Pergunta até a pessoa digitar um inteiro de min a max.
// Devolve true e guarda o número em *saida.
// Devolve false se a entrada acabou (Ctrl+D).
bool ler_int(const char *pergunta, int min, int max, int *saida) {
char linha[100];
while (true) {
printf("%s", pergunta);
if (fgets(linha, sizeof linha, stdin) == NULL) {
return false;
}
linha[strcspn(linha, "\n")] = '\0';
char *fim;
errno = 0;
long n = strtol(linha, &fim, 10);
if (fim == linha || *fim != '\0') {
printf("\"%s\" não é um número inteiro. Tente de novo.\n", linha);
} else if (errno == ERANGE || n < min || n > max) {
printf("Digite um número de %d a %d.\n", min, max);
} else {
*saida = (int)n;
return true;
}
}
}
int main(void) {
int qtd = 0;
int dia = 0;
if (!ler_int("Quantos pães (1 a 50)? ", 1, 50, &qtd) ||
!ler_int("Dia da entrega (1 a 31)? ", 1, 31, &dia)) {
printf("\nPedido cancelado.\n");
return 1;
}
printf("Pedido: %d pães para o dia %d.\n", qtd, dia);
return 0;
}
- bool ler_int(…)
- A função devolve um
bool:true= "consegui um número válido",false= "a entrada acabou, não há número". Assim, quem chama sabe se pode usar o resultado. - const char *pergunta
- O texto da pergunta.
constpromete que a função não vai mudá-lo (aula Ponteiros e Strings). - int min, int max
- A faixa aceita. A mesma função serve para pães (1 a 50) e para dias (1 a 31).
- int *saida
- Onde guardar o número. O valor de retorno já está ocupado com o
true/false, então o número sai por um ponteiro, como noscanf(aula Passagem por Ponteiro). - *saida = (int)n; return true;
- Só escreve no
*saidaquando o número é válido. O(int)converte olongdostrtolparaint; é seguro, porque o número já está entreminemax, que sãoint. - if (!ler_int(…) || !ler_int(…))
- "Se a primeira leitura falhar ou a segunda falhar…". O
||para na primeira que dertrue: se a entrada acabou na quantidade, o programa nem pergunta o dia.
$ gcc -std=c23 -Wall -Wextra pedido.c -o pedido $ ./pedido Quantos pães (1 a 50)? meia dúzia "meia dúzia" não é um número inteiro. Tente de novo. Quantos pães (1 a 50)? 6 Dia da entrega (1 a 31)? 0 Digite um número de 1 a 31. Dia da entrega (1 a 31)? 32 Digite um número de 1 a 31. Dia da entrega (1 a 31)? 15 Pedido: 6 pães para o dia 15. $ ./pedido Quantos pães (1 a 50)? 6 Dia da entrega (1 a 31)? « Ctrl+D Pedido cancelado. $ echo $? 1
O main ficou curto e legível: ele só diz o que perguntar. O como (ler, converter, conferir, repetir) está num lugar só. Se um dia você quiser melhorar a validação, muda uma função e todas as perguntas melhoram juntas.
Experimente: esta caixa não tem entrada pronta: rode e responda no terminal da caixa. Tente tudo o que uma pessoa distraída faria: seis, só Enter, 6 (com espaço no fim), 0, 51, -1, e no fim um pedido válido. Depois rode de novo e aperte Ctrl+D no campo de digitação, na primeira pergunta.
#include <errno.h>
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
bool ler_int(const char *pergunta, int min, int max, int *saida) {
char linha[100];
while (true) {
printf("%s", pergunta);
if (fgets(linha, sizeof linha, stdin) == NULL) {
return false;
}
linha[strcspn(linha, "\n")] = '\0';
char *fim;
errno = 0;
long n = strtol(linha, &fim, 10);
if (fim == linha || *fim != '\0') {
printf("\"%s\" não é um número inteiro. Tente de novo.\n", linha);
} else if (errno == ERANGE || n < min || n > max) {
printf("Digite um número de %d a %d.\n", min, max);
} else {
*saida = (int)n;
return true;
}
}
}
int main(void) {
int qtd = 0;
int dia = 0;
if (!ler_int("Quantos pães (1 a 50)? ", 1, 50, &qtd) ||
!ler_int("Dia da entrega (1 a 31)? ", 1, 31, &dia)) {
printf("\nPedido cancelado.\n");
return 1;
}
printf("Pedido: %d pães para o dia %d.\n", qtd, dia);
return 0;
}
Você deve ter visto que 6 , com um espaço no fim, foi recusado: o espaço é "sobra" para o *fim != '\0'. Se quiser aceitar, pule os espaços do fim antes de conferir, com while (isspace((unsigned char)*fim)) fim++; (o isspace está na seção 6).
5. Quando a linha é maior que o array
O fgets(linha, sizeof linha, stdin) nunca escreve além do array: é por isso que ele é seguro. Mas o que não coube não some: fica na entrada e vira a próxima "linha". Para ver isso sem digitar 100 caracteres, troque o array do pedido.c por char linha[8]; (cabem 7 caracteres e o '\0'):
$ gcc -std=c23 -Wall -Wextra curta.c -o curta $ ./curta Quantos pães (1 a 50)? 123456789012 Digite um número de 1 a 50. « leu só "1234567" Quantos pães (1 a 50)? Digite um número de 1 a 50. « leu "89012" sem esperar Quantos pães (1 a 50)? 6 Dia da entrega (1 a 31)? 4 Pedido: 6 pães para o dia 4.
A pergunta apareceu duas vezes por uma única resposta. Com char linha[100] acontece o mesmo, só que a partir do 100º caractere (uma pessoa que cola um texto, ou um arquivo com uma linha enorme). Para descobrir que a linha não coube, olhe se o Enter chegou: se o fgets leu o '\n', a linha inteira está no array; se não, ainda há mais na entrada.
if (fgets(linha, sizeof linha, stdin) == NULL) {
return false;
}
char *enter = strchr(linha, '\n');
if (enter != NULL) {
*enter = '\0';
} else {
int c = getchar();
if (c != '\n' && c != EOF) {
while ((c = getchar()) != '\n' && c != EOF) {
// joga fora o resto da linha
}
printf("Linha comprida demais. Tente de novo.\n");
continue;
}
}
- char *enter = strchr(linha, '\n');
- Procura o Enter no que foi lido (aula Funções de String). Achou: a linha coube, e o
*enter = '\0'o apaga, como fazia ostrcspn. - int c = getchar();
- Não achou o Enter. Ainda pode estar tudo bem: a linha tinha exatamente 7 caracteres e o Enter ficou de fora, ou era a última linha de um arquivo, sem Enter. O
getcharespia o próximo caractere para decidir. - if (c != '\n' && c != EOF)
- O próximo não é o Enter nem o fim: a linha era comprida mesmo. O laço de sempre joga fora o resto.
- continue;
- Volta para o começo do
while (true)e pergunta de novo, sem tentar converter o pedaço.
$ gcc -std=c23 -Wall -Wextra comprida.c -o comprida $ ./comprida Quantos pães (1 a 50)? 123456789012 Linha comprida demais. Tente de novo. Quantos pães (1 a 50)? 1234567 Digite um número de 1 a 50. « 7 caracteres: coube, e foi recusado pela faixa Quantos pães (1 a 50)? 6 Pedido: 6 pães.
Agora uma resposta comprida gera uma mensagem e uma nova pergunta. Na prática, com char linha[100], muitos programas pequenos aceitam o risco e usam só o strcspn. Programas que leem arquivos ou texto colado devem tratar a linha comprida.
6. Validar texto: em branco e "s ou n"
Nem toda entrada é número. Um nome não pode ficar em branco, e uma pergunta de sim ou não só aceita duas respostas. As ferramentas são as funções de caractere do ctype.h (aula char): isspace ("is space", é espaço?) e tolower ("to lower", para minúscula).
#include <ctype.h>
#include <stdio.h>
#include <string.h>
// true se o texto está vazio ou só tem espaços
bool so_espacos(const char *s) {
for (; *s != '\0'; s++) {
if (!isspace((unsigned char)*s)) {
return false;
}
}
return true;
}
bool ler_nome(const char *pergunta, char *nome, int tamanho) {
while (true) {
printf("%s", pergunta);
if (fgets(nome, tamanho, stdin) == NULL) {
return false;
}
nome[strcspn(nome, "\n")] = '\0';
if (!so_espacos(nome)) {
return true;
}
printf("O nome não pode ficar em branco.\n");
}
}
// Devolve 's' ou 'n', ou 0 se a entrada acabou.
char ler_sim_nao(const char *pergunta) {
char linha[20];
while (true) {
printf("%s", pergunta);
if (fgets(linha, sizeof linha, stdin) == NULL) {
return 0;
}
linha[strcspn(linha, "\n")] = '\0';
char r = (char)tolower((unsigned char)linha[0]);
if (strlen(linha) == 1 && (r == 's' || r == 'n')) {
return r;
}
printf("Responda s ou n.\n");
}
}
int main(void) {
char nome[50];
if (!ler_nome("Nome do cliente: ", nome, sizeof nome)) {
return 1;
}
char entrega = ler_sim_nao("Entregar em casa? (s/n) ");
if (entrega == 0) {
return 1;
}
printf("Cliente: %s, entrega: %s\n", nome, entrega == 's' ? "sim" : "não");
return 0;
}
- for (; *s != '\0'; s++)
- Anda pela string caractere por caractere, até o
'\0'do fim (aula Ponteiros e Strings). - isspace((unsigned char)*s)
- Verdadeiro para espaço, tab e Enter. O
(unsigned char)é obrigatório nas funções doctype.h: letras acentuadas como oéviram números negativos numchar, e um valor negativo nessas funções é comportamento indefinido. - if (!so_espacos(nome)) return true;
- Achou pelo menos um caractere que não é espaço: o nome serve.
- tolower(…linha[0])
- Transforma
Sems: a pessoa pode responder em maiúscula ou minúscula. - strlen(linha) == 1 && (r == 's' || r == 'n')
- Exatamente um caractere, e ele é
soun. Sem ostrlen,socorrovaleria como "sim". - return 0;
- O caractere de código 0 (o
'\0') não é's'nem'n', então serve de sinal para "a entrada acabou".
$ gcc -std=c23 -Wall -Wextra cadastro.c -o cadastro $ ./cadastro Nome do cliente: « só Enter O nome não pode ficar em branco. Nome do cliente: « três espaços O nome não pode ficar em branco. Nome do cliente: Ana Maria Entregar em casa? (s/n) talvez Responda s ou n. Entregar em casa? (s/n) S Cliente: Ana Maria, entrega: sim
Formas diferentes, e qual usar
| Forma | Bom para | Cuidado |
|---|---|---|
scanf + confere o retorno + encerra | Exercícios rápidos | Um erro de digitação encerra o programa |
scanf + descarta a linha + pede de novo (seção 2) | Programas pequenos que já usam scanf | Aceita 8abc como 8 e 7.5 como 7 |
fgets + strtol + faixa (seções 3 e 4) | A escolha padrão para qualquer entrada digitada | Lembre do errno = 0 e de tirar o \n |
| … + tratar a linha comprida (seção 5) | Texto colado, arquivos, programas que outras pessoas usam | Um pouco mais de código |
fgets + strtod + faixa | Números com casas decimais (preço, peso) | Só aceita ponto: 4,99 sobra ,99 |
fgets + sscanf | Formatos com várias partes (17/10/2026) | Confira o retorno; não detecta número grande demais |
E quantas vezes perguntar? Num programa de terminal, perguntar até acertar é o normal: a pessoa sempre pode desistir com Ctrl+D ou Ctrl+C. Uma senha, por outro lado, costuma ter um limite de tentativas (exercício 6). E quando a entrada vem de um arquivo ou de argumentos, não há ninguém para responder: o certo é mostrar o erro e encerrar com código diferente de 0 (aula exit e atexit).
Quando dá errado
while (nota < 0 || nota > 10) {
printf("Nota (0 a 10): ");
if (scanf("%d", ¬a) != 1) {
printf("Isso não é um número. Tente de novo.\n");
}
}
$ ./semdescarte Nota (0 a 10): oito Isso não é um número. Tente de novo. Nota (0 a 10): Isso não é um número. Tente de novo. Nota (0 a 10): Isso não é um número. Tente de novo. Nota (0 a 10): Isso não é um número. Tente de novo. ^C « até a Ana apertar Ctrl+C
O que significa: o scanf falhou e deixou o oito na entrada. Cada nova tentativa encontra o mesmo oito.
Como consertar: jogue fora o resto da linha depois de cada scanf (seção 2), ou troque por fgets + strtol (seção 3).
Nota (0 a 10): sem parar
do {
printf("Nota (0 a 10): ");
fgets(linha, sizeof linha, stdin);
linha[strcspn(linha, "\n")] = '\0';
nota = strtol(linha, &fim, 10);
} while (fim == linha || *fim != '\0' || nota < 0 || nota > 10);
$ ./semnull Nota (0 a 10): oito Nota (0 a 10): « Ctrl+D Nota (0 a 10): Nota (0 a 10): Nota (0 a 10): Nota (0 a 10): Nota (0 a 10): Nota (0 a 10): Nota (0 a 10): Nota (0 a 10): Nota (0 a 10): …
O que significa: ninguém conferiu o retorno do fgets. Quando a entrada acaba, ele devolve NULL na hora, toda vez, sem mexer no array, que continua com o oito antigo. O laço pergunta para sempre para ninguém. O gcc avisa se você compilar com otimização (-O2), porque aí o Ubuntu liga as conferências extras da glibc:
$ gcc -std=c23 -Wall -Wextra -O2 semnull.c -o semnull
semnull.c: In function ‘main’:
semnull.c:11:9: warning: ignoring return value of ‘fgets’ declared with attribute ‘warn_unused_result’ [-Wunused-result]
11 | fgets(linha, sizeof linha, stdin);
| ^~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
"Ignorando o valor de retorno de fgets, declarada com o atributo warn_unused_result" (avise se o resultado não for usado). O mesmo aviso aparece para o scanf do confia.c da seção 1.
Como consertar: if (fgets(…) == NULL), e aí encerre ou devolva false, como a ler_int.
$ ./semtirar Idade: 29 "29 " não é um número inteiro.
O que significa: faltou tirar o Enter que o fgets guarda. O strtol parou no \n, o *fim != '\0' deu verdadeiro, e o \n também apareceu dentro das aspas da mensagem.
Como consertar: linha[strcspn(linha, "\n")] = '\0'; logo depois do fgets.
8abc virou 8 e 7.5 virou 7
O que significa: o programa usa scanf("%d"), que lê o começo da linha e para no primeiro caractere que não é dígito. O descarte joga fora o resto sem olhar.
Como consertar: leia a linha com fgets e confira com *fim != '\0' (seção 3).
O que significa: a resposta anterior não coube no array do fgets, e o pedaço que sobrou foi lido como uma nova resposta. Outra causa comum é um scanf antes do fgets, que deixa o Enter na entrada (aula Ler uma Linha).
Como consertar: confira se o '\n' chegou e jogue fora o resto (seção 5); e não misture scanf com fgets.
char *fim;
int idade = strtol(linha, &fim, 10);
if (fim == linha || *fim != '\0' || idade < 0 || idade > 130) {
printf("Idade inválida.\n");
return 1;
}
$ gcc -std=c23 -Wall -Wextra estouro.c -o estouro $ ./estouro Idade: 4294967326 Idade registrada: 30
O que significa: o strtol devolve um long, que no Linux de 64 bits guarda 4294967326 sem problema. Mas o valor foi guardado num int antes de conferir a faixa, e um int só vai até 2147483647: o número "deu a volta" e virou 30, que passou na conferência. Nenhum aviso do compilador. (No editor do guia o long tem 4 bytes, o strtol já estoura e o mesmo programa mostra Idade inválida.)
Como consertar: guarde o resultado num long, confira errno e a faixa nele, e só depois converta para int, como a ler_int faz com *saida = (int)n;.
Referência rápida
| Escrita | O que faz |
|---|---|
int lidos = scanf("%d", &n); | 1 = leu; 0 = não era número; EOF = a entrada acabou |
while ((c = getchar()) != '\n' && c != EOF) { } | Joga fora o resto da linha (com int c) |
if (fgets(linha, sizeof linha, stdin) == NULL) | A entrada acabou: pare de perguntar |
linha[strcspn(linha, "\n")] = '\0'; | Tira o Enter |
errno = 0; n = strtol(linha, &fim, 10); | Converte, marcando onde parou |
fim == linha || *fim != '\0' | Não era só um número |
errno == ERANGE || n < min || n > max | Fora da faixa |
strchr(linha, '\n') == NULL | A linha talvez não tenha cabido (confira com getchar) |
isspace((unsigned char)c), tolower((unsigned char)c) | É espaço? Para minúscula. (ctype.h) |
bool ler_int(const char *pergunta, int min, int max, int *saida) | O modelo desta aula: pergunta até valer; false no fim da entrada |
Exercícios
1. Fim da entrada
Complete a condição que é verdadeira quando a entrada acabou:
2. Sobrou texto
Depois de strtol(linha, &fim, 10), complete o teste que é verdadeiro quando sobrou texto depois do número:
3. O retorno do scanf
A Ana digitou abc. Quanto vale lidos em int lidos = scanf("%d", &n);?
4. Com a ler_int da seção 4 e a faixa de 1 a 50, qual destas respostas é aceita?
5. Por que a ler_int devolve false quando o fgets devolve NULL, em vez de perguntar de novo?
6. A Ana quer que a senha tenha só 3 tentativas. Qual laço faz isso?
Missão final: o lançamento de notas
Missão: a professora Ana lança três notas, de 0 a 10. A ler_int deste programa usa atoi e aceita qualquer coisa: oito virou 0 e 15 passou. Troque o atoi por strtol com as conferências da seção 4: se não for número, mostre "oito" não é um número inteiro. Tente de novo.; se estiver fora da faixa, Digite um número de 0 a 10.; e só devolva true com um número válido. Os avisos de min e max não usados somem quando você usar os dois. A entrada já está pronta (oito, 15, 7, 8.5, 9, 5), e a saída deve terminar com:
Média: 7.0
#include <errno.h>
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
bool ler_int(const char *pergunta, int min, int max, int *saida) {
char linha[100];
while (true) {
printf("%s", pergunta);
if (fgets(linha, sizeof linha, stdin) == NULL) {
return false;
}
linha[strcspn(linha, "\n")] = '\0';
// troque o atoi por strtol e confira se é um número
// e se está entre min e max
*saida = atoi(linha);
return true;
}
}
int main(void) {
int notas[3];
for (int i = 0; i < 3; i++) {
char pergunta[40];
snprintf(pergunta, sizeof pergunta, "Nota %d (0 a 10): ", i + 1);
if (!ler_int(pergunta, 0, 10, ¬as[i])) {
printf("\nLançamento cancelado.\n");
return 1;
}
}
printf("Média: %.1f\n", (notas[0] + notas[1] + notas[2]) / 3.0);
return 0;
}
#include <errno.h>
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
bool ler_int(const char *pergunta, int min, int max, int *saida) {
char linha[100];
while (true) {
printf("%s", pergunta);
if (fgets(linha, sizeof linha, stdin) == NULL) {
return false;
}
linha[strcspn(linha, "\n")] = '\0';
char *fim;
errno = 0;
long n = strtol(linha, &fim, 10);
if (fim == linha || *fim != '\0') {
printf("\"%s\" não é um número inteiro. Tente de novo.\n", linha);
} else if (errno == ERANGE || n < min || n > max) {
printf("Digite um número de %d a %d.\n", min, max);
} else {
*saida = (int)n;
return true;
}
}
}
int main(void) {
int notas[3];
for (int i = 0; i < 3; i++) {
char pergunta[40];
snprintf(pergunta, sizeof pergunta, "Nota %d (0 a 10): ", i + 1);
if (!ler_int(pergunta, 0, 10, ¬as[i])) {
printf("\nLançamento cancelado.\n");
return 1;
}
}
printf("Média: %.1f\n", (notas[0] + notas[1] + notas[2]) / 3.0);
return 0;
}
No laboratório, o programa pronto dá a mesma saída:
$ gcc -std=c23 -Wall -Wextra notas.c -o notas $ ./notas Nota 1 (0 a 10): oito "oito" não é um número inteiro. Tente de novo. Nota 1 (0 a 10): 15 Digite um número de 0 a 10. Nota 1 (0 a 10): 7 Nota 2 (0 a 10): 8.5 "8.5" não é um número inteiro. Tente de novo. Nota 2 (0 a 10): 9 Nota 3 (0 a 10): 5 Média: 7.0
Resumo
- Nunca confie na entrada: confira, explique o problema (com a faixa certa) e pergunte de novo.
- Com
scanf: confira o retorno (1,0ouEOF) e jogue fora o resto da linha com o laço dogetchar. Ainda assim,8abcvira 8. - O jeito robusto:
fgets(comNULL= fim da entrada), tirar o\n,strtolcomfimeerrno, e conferir a faixa. - Ponha a receita numa função como
bool ler_int(pergunta, min, max, &saida)e reaproveite. - Quando o
fgetsnão leu o'\n', a linha pode não ter cabido: jogue fora o resto. - Texto também se valida: em branco com
isspace, respostas fixas comtolowerestrlen. Nas funções doctype.h, use(unsigned char). - Quando a entrada acaba, pare de perguntar: senão o programa entra num laço infinito.