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.

Você vai aprender
  • 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 com strtol e conferir a faixa.
  • Uma função ler_int para 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…ExemploO programa deve…
digitou texto no lugar de númeroseisdizer que não é número e pedir de novo
apertou Enter sem escrever nada(linha vazia)pedir de novo
escreveu um número com sobra6abc, 7.5, 20 anosrecusar a linha inteira
escreveu um número fora da faixa-3, 15 numa nota de 0 a 10dizer qual é a faixa e pedir de novo
escreveu um número enorme99999999999999999999tratar como fora da faixa
colou um texto maior que o array200 caracteresjogar fora o excesso (seção 5)
encerrou a entradaCtrl+Dparar 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", &nota);
        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 pelo return (entrada acabou). Veja break e continue. true é palavra-chave desde o C23; antes, era preciso #include <stdbool.h> ou escrever while (1).
int lidos = scanf("%d", &nota);
Guarda o que o scanf devolve: 1 se leu o número, 0 se 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 \n no 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 oito que travaria o scanf, e também o \n de 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", &nota);
        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", &nota);
        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:

  1. Ler a linha inteira com fgets. Se ele devolver NULL, a entrada acabou.
  2. Converter com strtol e conferir se a linha era só um número (o fim da aula Texto ↔ Número).
  3. 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;
}
LinhaO que faz
fgets(linha, sizeof linha, stdin) == NULLPasso 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 > 130Passo 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.

Escreva a faixa na mensagem. "Valor inválido" obriga a pessoa a adivinhar. "A idade vai de 0 a 130" diz o que fazer. Uma boa mensagem de erro de entrada responde a duas perguntas: o que estava errado e o que a pessoa deve digitar.

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. const promete 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 no scanf (aula Passagem por Ponteiro).
*saida = (int)n; return true;
Só escreve no *saida quando o número é válido. O (int) converte o long do strtol para int; é seguro, porque o número já está entre min e max, que são int.
if (!ler_int(…) || !ler_int(…))
"Se a primeira leitura falhar ou a segunda falhar…". O || para na primeira que der true: 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 o strcspn.
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 getchar espia 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 do ctype.h: letras acentuadas como o é viram números negativos num char, 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 S em s: a pessoa pode responder em maiúscula ou minúscula.
strlen(linha) == 1 && (r == 's' || r == 'n')
Exatamente um caractere, e ele é s ou n. Sem o strlen, socorro valeria 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

FormaBom paraCuidado
scanf + confere o retorno + encerraExercícios rápidosUm erro de digitação encerra o programa
scanf + descarta a linha + pede de novo (seção 2)Programas pequenos que já usam scanfAceita 8abc como 8 e 7.5 como 7
fgets + strtol + faixa (seções 3 e 4)A escolha padrão para qualquer entrada digitadaLembre do errno = 0 e de tirar o \n
… + tratar a linha comprida (seção 5)Texto colado, arquivos, programas que outras pessoas usamUm pouco mais de código
fgets + strtod + faixaNúmeros com casas decimais (preço, peso)Só aceita ponto: 4,99 sobra ,99
fgets + sscanfFormatos 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

A pergunta se repete sem parar depois de uma letra
    while (nota < 0 || nota > 10) {
        printf("Nota (0 a 10): ");
        if (scanf("%d", &nota) != 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).

Depois do Ctrl+D, 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.

Todo número é recusado, e a mensagem sai quebrada em duas linhas
$ ./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).

A pergunta aparece duas vezes seguidas, sem esperar

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.

A idade 4294967326 foi aceita como 30
    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

EscritaO 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 > maxFora da faixa
strchr(linha, '\n') == NULLA 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:

if (fgets(linha, sizeof linha, stdin) == )

2. Sobrou texto

Depois de strtol(linha, &fim, 10), complete o teste que é verdadeiro quando sobrou texto depois do número:

*fim '\0'

3. O retorno do scanf

A Ana digitou abc. Quanto vale lidos em int lidos = scanf("%d", &n);?

lidos =

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, &notas[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, &notas[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, 0 ou EOF) e jogue fora o resto da linha com o laço do getchar. Ainda assim, 8abc vira 8.
  • O jeito robusto: fgets (com NULL = fim da entrada), tirar o \n, strtol com fim e errno, e conferir a faixa.
  • Ponha a receita numa função como bool ler_int(pergunta, min, max, &saida) e reaproveite.
  • Quando o fgets nã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 com tolower e strlen. Nas funções do ctype.h, use (unsigned char).
  • Quando a entrada acaba, pare de perguntar: senão o programa entra num laço infinito.