C fgets: ler uma linha

O scanf("%s", …) lê só uma palavra: se a Ana digita "Ana Maria", ele fica com "Ana". Para ler uma linha inteira, com espaços, o C tem o fgets. Ele é também o jeito seguro de ler texto, porque você diz o tamanho do array e ele nunca passa disso. Nesta aula você vai usar o fgets, entender o \n que ele traz junto (e tirá-lo numa linha), descobrir por que o antigo gets foi expulso da linguagem e resolver o problema mais perguntado de C nos fóruns: "meu fgets não espera eu digitar!".

Você vai aprender
  • A receita fgets(linha, sizeof linha, stdin) e o que cada parte faz.
  • Por que a linha lida termina com \n, e como tirá-lo com linha[strcspn(linha, "\n")] = '\0';.
  • O que acontece quando a linha é maior que o array.
  • Por que o gets foi removido no C11, e o que o gcc 15 diz se você usá-lo.
  • O \n que o scanf deixa para trás e faz o fgets "pular", e dois consertos.
  • O retorno NULL no fim da entrada (Ctrl+D) e como ler linha por linha até o fim.

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 o comando padrão do guia: gcc -std=c23 -Wall -Wextra arquivo.c -o programa. Nos exemplos, às vezes a entrada vem de um echo "…" |, que manda o texto para o programa como se a Ana o tivesse digitado; por isso a resposta aparece grudada na pergunta.


A ideia: um balde com medida

Imagine encher um balde numa torneira. O scanf("%s") sem limite é uma torneira que não para: se a água for mais do que cabe, transborda e molha o chão (a memória vizinha). O fgets é um balde com a marca "até aqui": você diz quanto cabe, ele enche até a marca ou até o fim da linha, o que vier primeiro, e fecha a torneira. O que sobrar fica esperando na torneira para o próximo balde.

O nome vem de file get string, "pegar uma string de um arquivo". O "arquivo" aqui é o teclado: em C, o teclado se chama stdin (standard input, "entrada padrão", aula stdin, stdout e stderr). O mesmo fgets lê linhas de arquivos de verdade, na aula Ler Arquivos.


1. A primeira leitura

#include <stdio.h>

int main(void) {
    char nome[50];
    printf("Nome completo: ");
    if (fgets(nome, sizeof nome, stdin) == NULL) {
        return 1;
    }
    printf("Olá, %s!\n", nome);
    return 0;
}
char nome[50];
O balde: espaço para até 49 caracteres + o '\0' (aula Strings).
fgets(
"Leia uma linha…"
nome,
1º argumento: onde guardar (sem &, como toda string).
sizeof nome,
2º argumento: o tamanho do array, 50. O fgets lê no máximo 49 caracteres e reserva o último lugar para o '\0'. Use sempre sizeof do próprio array: se um dia o array mudar de tamanho, o número muda junto.
stdin)
3º argumento: de onde ler. stdin é o teclado.
== NULL
O fgets devolve NULL ("nulo") quando não consegue ler nada: a entrada acabou (seção 6). Confira sempre, como você confere o retorno do scanf.
$ gcc -std=c23 -Wall -Wextra ler.c -o ler
$ ./ler
Nome completo: Ana Maria
Olá, Ana Maria
!                        « o "!" foi parar na linha de baixo

O nome veio inteiro, com o espaço. Mas o ! pulou de linha. Por quê?


2. O \n que vem junto

Quando a Ana termina de digitar, ela aperta Enter. O Enter também é um caractere: o '\n' (nova linha, aula Escapes). O fgets lê até o Enter, inclusive, e guarda o '\n' na string. Veja os números:

#include <stdio.h>
#include <string.h>

int main(void) {
    char linha[20];
    if (fgets(linha, sizeof linha, stdin) == NULL) {
        return 1;
    }
    size_t n = strlen(linha);
    printf("strlen = %zu\n", n);
    printf("último caractere: código %d\n", linha[n - 1]);
    return 0;
}
$ gcc -std=c23 -Wall -Wextra bytesn.c -o bytesn
$ echo "Ana Maria" | ./bytesn
strlen = 10              « 9 letras e espaço + o '\n'
último caractere: código 10     « 10 é o código do '\n'

A memória do array ficou assim:

Índice012345678910
CaractereAna␣Maria'\n''\0'

Por que o fgets guarda o Enter? Para você saber se a linha veio inteira. Se a string termina com '\n', a linha coube toda no balde; se não termina, ela era maior que o array e o resto ficou esperando (seção 3).

Tirar o \n com strcspn

Na maioria das vezes você não quer o '\n': quer comparar o nome, mostrar no meio de uma frase, guardar num arquivo. O jeito mais usado de tirá-lo é uma linha só:

#include <stdio.h>
#include <string.h>

int main(void) {
    char nome[50];
    printf("Nome completo: ");
    if (fgets(nome, sizeof nome, stdin) == NULL) {
        return 1;
    }
    nome[strcspn(nome, "\n")] = '\0';
    printf("Olá, %s!\n", nome);
    printf("(%zu caracteres)\n", strlen(nome));
    return 0;
}
#include <string.h>
O strcspn e o strlen moram aqui (aula Funções de String).
strcspn(nome, "\n")
strcspn = "string complement span", algo como "quantos caracteres do começo não estão nesta lista". Ele conta as letras de nome até achar um '\n'. Para "Ana Maria\n", devolve 9: a posição do '\n'.
nome[…] = '\0';
Coloca a placa de fim nessa posição, por cima do '\n'. É o truque de "cortar com '\0'" da aula Strings.
E se não houver '\n'?
Aí o strcspn conta até o fim e devolve a posição do '\0' que já existe. Trocar um '\0' por '\0' não muda nada. Por isso essa linha é segura em qualquer caso, até com a string vazia.
$ gcc -std=c23 -Wall -Wextra tirar.c -o tirar
$ ./tirar
Nome completo: Ana Maria
Olá, Ana Maria!
(9 caracteres)
Por que não nome[strlen(nome) - 1] = '\0';?

Você vai encontrar essa versão em muito código. Ela apaga o último caractere sem conferir se é um '\n': se a linha veio cortada (sem '\n'), ela apaga uma letra de verdade. E se a string estiver vazia, strlen dá 0 e 0 - 1 num size_t vira um número enorme: escrita fora do array. A versão com strcspn não tem nenhum dos dois problemas.


3. Linha maior que o array

O fgets nunca passa do tamanho que você deu. Com um array de 8, ele lê no máximo 7 caracteres por vez, e o resto da linha fica na entrada para a próxima leitura. Este programa lê pedaço por pedaço até a entrada acabar:

#include <stdio.h>

int main(void) {
    char pedaco[8];
    while (fgets(pedaco, sizeof pedaco, stdin) != NULL) {
        printf("[%s]\n", pedaco);
    }
    return 0;
}
$ gcc -std=c23 -Wall -Wextra pedacos.c -o pedacos
$ echo "arroz integral tipo 1" | ./pedacos
[arroz i]                « 7 caracteres + '\0'
[ntegral]
[ tipo 1]
[                        « o último pedaço é só o '\n'
]

Nenhum byte foi escrito fora do array: cada leitura parou em 7 caracteres e pôs o '\0'. A linha de 21 caracteres + Enter chegou em quatro pedaços, e só o último trouxe o '\n'. Na prática:

  • Escolha um array folgado para o que você espera (100, 256 bytes para uma linha de texto comum).
  • Se a linha não terminou em '\n', ela foi cortada. Você decide: ler o resto, jogar fora ou avisar o usuário (aula Validar a Entrada).

Experimente: rode e veja os pedaços. Depois troque o tamanho do array: com [4] saem mais pedaços; com [100], um só (com o \n dentro dos colchetes). Você também pode mudar a entrada no campo Entrada.

#include <stdio.h>

int main(void) {
    char pedaco[8];
    while (fgets(pedaco, sizeof pedaco, stdin) != NULL) {
        printf("[%s]\n", pedaco);
    }
    return 0;
}

4. Por que o gets foi removido

Antes do fgets, os livros ensinavam o gets ("get string"): gets(nome);. Ele lê uma linha inteira e tira o '\n' sozinho. Parece melhor, mas tem um defeito que não tem conserto: ele não recebe o tamanho do array. Não existe jeito de usá-lo com segurança. Qualquer linha maior que o array escreve além do fim, e quem decide o tamanho da linha é quem digita.

O gets foi marcado como obsoleto no C99 e removido da linguagem no C11: é a única função da biblioteca padrão que já foi apagada. Veja o que o gcc 15 faz com este programa:

#include <stdio.h>

int main(void) {
    char nome[10];
    printf("Nome: ");
    gets(nome);
    printf("Olá, %s!\n", nome);
    return 0;
}
$ gcc -std=c23 -Wall -Wextra gets.c -o gets
gets.c: In function ‘main’:
gets.c:6:5: error: implicit declaration of function ‘gets’; did you mean ‘fgets’? [-Wimplicit-function-declaration]
    6 |     gets(nome);
      |     ^~~~
      |     fgets

Implicit declaration of function ‘gets’; did you mean ‘fgets’? = "declaração implícita da função gets; você quis dizer fgets?". No C23 o stdio.h nem declara mais o gets, então o compilador não o conhece e já sugere o substituto. O Clang do editor diz use of undeclared identifier 'gets'.

Pedindo um padrão antigo, em que o gets ainda existia, o programa compila, mas com dois avisos, um do compilador e outro do ligador (que sai em português no laboratório):

$ gcc -std=c99 -Wall -Wextra gets.c -o gets
gets.c: In function ‘main’:
gets.c:6:5: warning: ‘gets’ is deprecated [-Wdeprecated-declarations]
    6 |     gets(nome);
      |     ^~~~
In file included from gets.c:1:
/usr/include/stdio.h:671:14: note: declared here
  671 | extern char *gets (char *__s) __wur __attribute_deprecated__;
      |              ^~~~
/usr/bin/x86_64-linux-gnu-ld.bfd: /tmp/ccp26XUo.o: na função "main":
gets.c:(.text+0x37): aviso: the `gets' function is dangerous and should not be used.
$ echo "Ana" | ./gets
Nome: Olá, Ana!
$ echo "Maximiliano de Albuquerque" | ./gets; echo "código $?"
*** stack smashing detected ***: terminated
código 134
  • ‘gets’ is deprecated = "gets está obsoleto".
  • The `gets' function is dangerous and should not be used = "a função gets é perigosa e não deve ser usada". É o ligador (ld) que avisa, porque o aviso vem gravado na própria biblioteca.
  • Com "Ana" funcionou. Com 26 letras num array de 10, o gets estourou o array e a proteção da pilha abortou o programa (código 134), o mesmo estouro de buffer da aula Funções de String.

Missão: este programa veio de um livro antigo e nem compila mais. Troque o gets por fgets (com o tamanho do array e stdin) e tire o \n com strcspn, para mostrar Olá, Ana Maria! numa linha só. A entrada Ana Maria já está no campo Entrada.

#include <stdio.h>
#include <string.h>

int main(void) {
    char nome[50];
    printf("Nome: ");
    gets(nome);
    printf("Olá, %s!\n", nome);
    return 0;
}
#include <stdio.h>
#include <string.h>

int main(void) {
    char nome[50];
    printf("Nome: ");
    if (fgets(nome, sizeof nome, stdin) == NULL) {
        return 1;
    }
    nome[strcspn(nome, "\n")] = '\0';
    printf("Olá, %s!\n", nome);
    return 0;
}

5. Misturar scanf e fgets: o \n que sobra

Este é o problema de C mais perguntado nos fóruns. A Ana lê a idade com scanf e depois o nome com fgets:

#include <stdio.h>
#include <string.h>

int main(void) {
    int idade = 0;
    char nome[50];
    printf("Idade: ");
    if (scanf("%d", &idade) != 1) {
        return 1;
    }
    printf("Nome: ");
    if (fgets(nome, sizeof nome, stdin) == NULL) {
        return 1;
    }
    nome[strcspn(nome, "\n")] = '\0';
    printf("[%s] tem %d anos.\n", nome, idade);
    return 0;
}
$ gcc -std=c23 -Wall -Wextra mistura.c -o mistura
$ ./mistura
Idade: 29
Nome: [] tem 29 anos.    « nem deu tempo de digitar o nome!

O programa não esperou o nome. O que aconteceu, passo a passo:

  1. A Ana digitou 29 e apertou Enter. Na entrada ficou: 2 9 \n.
  2. O scanf("%d") leu o 29 e parou antes do \n, que não faz parte do número. O \n ficou na entrada.
  3. O fgets lê "até o Enter". O primeiro caractere que ele encontrou já era o \n que sobrou: leu uma linha vazia e voltou na hora.

Conserto 1: jogar fora o resto da linha depois do scanf

#include <stdio.h>
#include <string.h>

int main(void) {
    int idade = 0;
    char nome[50];
    printf("Idade: ");
    if (scanf("%d", &idade) != 1) {
        return 1;
    }
    int c;
    while ((c = getchar()) != '\n' && c != EOF) {
        // joga fora o resto da linha
    }
    printf("Nome: ");
    if (fgets(nome, sizeof nome, stdin) == NULL) {
        return 1;
    }
    nome[strcspn(nome, "\n")] = '\0';
    printf("[%s] tem %d anos.\n", nome, idade);
    return 0;
}
int c;
Um int, e não um char: o getchar devolve o código do caractere ou o valor especial EOF (end of file, fim da entrada), que não cabe num char.
(c = getchar())
Lê um caractere da entrada e guarda em c.
!= '\n' && c != EOF
Continua enquanto não chegar no Enter e a entrada não acabar. O laço não faz nada dentro: o trabalho é só ler e descartar.
$ gcc -std=c23 -Wall -Wextra conserto.c -o conserto
$ ./conserto
Idade: 29
Nome: Ana Maria
[Ana Maria] tem 29 anos.

Esse laço também joga fora qualquer lixo depois do número: com a entrada 29 anos, o scanf fica com o 29 e o laço descarta " anos" e o Enter.

Conserto 2 (o melhor): ler tudo com fgets

Muitos programadores experientes simplesmente não misturam: leem toda entrada com fgets, linha por linha, e depois tiram o número de dentro da linha com sscanf (um scanf que lê de uma string) ou strtol:

#include <stdio.h>
#include <string.h>

int main(void) {
    char linha[50];
    int idade = 0;
    char nome[50];

    printf("Idade: ");
    if (fgets(linha, sizeof linha, stdin) == NULL || sscanf(linha, "%d", &idade) != 1) {
        printf("Idade inválida.\n");
        return 1;
    }
    printf("Nome: ");
    if (fgets(nome, sizeof nome, stdin) == NULL) {
        return 1;
    }
    nome[strcspn(nome, "\n")] = '\0';
    printf("[%s] tem %d anos.\n", nome, idade);
    return 0;
}
$ gcc -std=c23 -Wall -Wextra sofgets.c -o sofgets
$ printf "29\nAna Maria\n" | ./sofgets
Idade: Nome: [Ana Maria] tem 29 anos.
$ printf "abc\nAna\n" | ./sofgets
Idade: Idade inválida.

Cada fgets consome a linha inteira, Enter incluído, então nada sobra para a leitura seguinte. E, se a pessoa digitar besteira, a linha errada já saiu da entrada. (O printf "29\nAna Maria\n" | manda duas linhas de uma vez, como se a Ana tivesse digitado as duas.) Transformar texto em número com segurança é o assunto da próxima aula, Texto ↔ Número.


6. O fim da entrada: NULL e o Ctrl+D

O fgets devolve NULL quando não há mais nada para ler. Isso acontece:

  • quando a entrada vem de um echo, de um printf ou de um arquivo e as linhas acabaram;
  • quando a pessoa aperta Ctrl+D no começo de uma linha, no terminal do Linux ou do macOS. É o jeito de dizer "acabou" pelo teclado. No Windows, é Ctrl+Z seguido de Enter.

Com isso dá para ler "quantas linhas vierem", sem saber o número antes:

#include <stdio.h>
#include <string.h>

int main(void) {
    char linha[100];
    int total = 0;
    while (fgets(linha, sizeof linha, stdin) != NULL) {
        total++;
        linha[strcspn(linha, "\n")] = '\0';
        printf("%d: %s\n", total, linha);
    }
    printf("Fim da entrada: %d linhas.\n", total);
    return 0;
}
$ gcc -std=c23 -Wall -Wextra linhas.c -o linhas
$ ./linhas
arroz
1: arroz
feijão
2: feijão
Fim da entrada: 2 linhas.     « aqui a Ana apertou Ctrl+D
$ printf "arroz\nfeijão\ncafé\n" | ./linhas
1: arroz
2: feijão
3: café
Fim da entrada: 3 linhas.
while (fgets(…) != NULL)
"Enquanto conseguir ler uma linha…". É a forma padrão de ler até o fim, em C, tanto do teclado quanto de arquivos.
Ctrl+D
Não aparece na tela. O terminal entende "fim da entrada", o fgets devolve NULL e o laço termina.

Quando o programa espera uma linha e a pessoa aperta Ctrl+D logo de cara, é o == NULL do começo da aula que evita usar um array que nunca recebeu nada:

$ cat vazio.c
#include <stdio.h>

int main(void) {
    char nome[50];
    printf("Nome: ");
    if (fgets(nome, sizeof nome, stdin) == NULL) {
        printf("\nNada foi digitado.\n");
        return 1;
    }
    printf("Olá, %s", nome);
    return 0;
}
$ gcc -std=c23 -Wall -Wextra vazio.c -o vazio
$ ./vazio
Nome:                    « Ctrl+D aqui
Nada foi digitado.
No editor do guia

Quando a caixa tem uma Entrada preenchida, o programa recebe aquelas linhas e depois o fim da entrada, como no printf "…" |. Sem entrada preenchida, o programa espera você digitar no terminal da caixa, e o Ctrl+D no campo de digitação também encerra a entrada.


Formas de ler texto: qual usar

FormaLêLimite de tamanho?Quando usar
fgets(s, sizeof s, stdin)Uma linha, com espaços e o '\n'SimPadrão do guia para qualquer texto digitado.
scanf("%49s", s)Uma palavra (para no espaço)Só se você escrever o númeroQuando a resposta é uma palavra só, e com o número.
scanf("%s", s)Uma palavraNãoNunca: estoura como o gets.
getchar()Um caractere(um por vez)Ler letra por letra, descartar o resto da linha.
gets(s)Uma linhaNãoNunca. Removido no C11.

Quando dá errado

A frase sai quebrada: Olá, Ana Maria e o resto na linha de baixo

O que significa: o '\n' do Enter ficou na string lida pelo fgets.

Como consertar: logo depois do fgets, s[strcspn(s, "\n")] = '\0'; (com #include <string.h>).

O fgets não espera eu digitar (lê uma linha vazia)

O que significa: um scanf anterior deixou o '\n' na entrada, e o fgets leu só ele.

Como consertar: depois do scanf, descarte o resto da linha com while ((c = getchar()) != '\n' && c != EOF) {}, ou leia tudo com fgets e converta com sscanf/strtol.

error: implicit declaration of function ‘gets’; did you mean ‘fgets’?

O que significa: o gets não existe mais no C11 em diante. Com -std=c99 ele compila, mas com warning: ‘gets’ is deprecated e o aviso do ligador the `gets' function is dangerous and should not be used.

Como consertar: fgets(s, sizeof s, stdin) e tirar o '\n' com strcspn.

A linha chega cortada, ou o resto aparece na próxima leitura

O que significa: a linha era maior que o array. O fgets leu sizeof s - 1 caracteres e deixou o resto na entrada.

Como consertar: use um array maior. Para detectar, confira se a string termina em '\n': se não terminar (e a entrada não acabou), a linha foi cortada.

warning: ‘sizeof’ on array function parameter ‘nome’ will return size of ‘char *’ [-Wsizeof-array-argument]

O que significa: o fgets(nome, sizeof nome, stdin) está dentro de uma função que recebeu nome como parâmetro (aula Parâmetros). Ali, nome não é mais o array, e sim um ponteiro para ele, e o sizeof mede o ponteiro: 8 bytes no Linux. No laboratório, com void ler_nome(char nome[]) e a entrada "Ana Maria", o programa guardou só [Ana Mar] (7 letras + '\0'). O motivo está em Ponteiros e Arrays.

Como consertar: use sizeof só onde o array foi criado. Na função, receba o tamanho como outro parâmetro: void ler_nome(char nome[], int tamanho) e fgets(nome, tamanho, stdin).


Referência rápida

EscritaO que faz
fgets(s, sizeof s, stdin)Lê uma linha (até sizeof s - 1 caracteres), com o '\n', e põe '\0'
retorno NULLNada foi lido: fim da entrada (Ctrl+D) ou erro
s[strcspn(s, "\n")] = '\0';Tira o '\n' do fim (seguro mesmo sem '\n')
strcspn(s, "\n")Posição do primeiro '\n' (ou do '\0', se não houver)
while (fgets(s, sizeof s, stdin) != NULL) { … }Lê linha por linha até o fim
while ((c = getchar()) != '\n' && c != EOF) {}Descarta o resto da linha (depois de um scanf)
sscanf(linha, "%d", &n)Lê um número de dentro de uma string já lida
Ctrl+D (Linux, macOS) / Ctrl+Z e Enter (Windows)Fim da entrada pelo teclado
gets(s)Removido no C11: nunca use

Exercícios

1. Os três argumentos

Complete a leitura segura de uma linha para o array frase:

fgets(frase, , );

2. Tirar o Enter

Complete a linha que tira o '\n' do fim de frase:

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

3. Quantos caracteres?

Com char s[10];, quantos caracteres (no máximo) um fgets(s, sizeof s, stdin) lê de uma vez? Escreva só o número.

4. A Ana digitou oi e Enter. Depois do fgets, quanto vale strlen(s)?

5. Por que o gets foi removido do C?

6. Depois de scanf("%d", &n);, um fgets devolveu uma linha vazia na hora. Por quê?

7. O que o fgets devolve quando a pessoa aperta Ctrl+D no começo da linha?

Missão final: a lista de compras

Missão: a Ana digita a lista de compras, um item por linha, e termina com Ctrl+D (aqui a entrada já está preenchida, com uma linha em branco no meio). Leia linha por linha com fgets até o fim, tire o \n, pule as linhas vazias e numere os itens. No fim, mostre o total.

1. arroz
2. feijão
3. café
Total: 3 itens
#include <stdio.h>
#include <string.h>

int main(void) {
    char linha[100];
    int itens = 0;

    // leia até o fim da entrada, pulando linhas vazias

    printf("Total: %d itens\n", itens);
    return 0;
}
#include <stdio.h>
#include <string.h>

int main(void) {
    char linha[100];
    int itens = 0;

    while (fgets(linha, sizeof linha, stdin) != NULL) {
        linha[strcspn(linha, "\n")] = '\0';
        if (linha[0] == '\0') {
            continue;
        }
        itens++;
        printf("%d. %s\n", itens, linha);
    }

    printf("Total: %d itens\n", itens);
    return 0;
}

Depois de tirar o '\n', uma linha em branco vira a string vazia: o primeiro caractere já é o '\0'. O continue (aula break e continue) pula para a próxima volta do laço sem contar o item.


Resumo

  • fgets(s, sizeof s, stdin) lê uma linha inteira, com espaços, até sizeof s - 1 caracteres, e sempre põe o '\0'. Nunca escreve fora do array.
  • A linha vem com o '\n' do Enter. Tire com s[strcspn(s, "\n")] = '\0';, que é seguro mesmo sem '\n'.
  • Linha maior que o array chega em pedaços; o que sobra fica na entrada. Sem '\n' no fim = linha cortada.
  • O gets não tem limite e foi removido no C11; o gcc 15 nem o reconhece (implicit declaration of function ‘gets’; did you mean ‘fgets’?).
  • O scanf deixa o '\n' na entrada e o fgets seguinte lê uma linha vazia. Descarte o resto da linha com getchar, ou leia tudo com fgets e converta com sscanf/strtol.
  • fgets devolve NULL no fim da entrada (Ctrl+D no Linux). while (fgets(…) != NULL) lê até o fim.