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!".
- 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 comlinha[strcspn(linha, "\n")] = '\0';. - O que acontece quando a linha é maior que o array.
- Por que o
getsfoi removido no C11, e o que o gcc 15 diz se você usá-lo. - O
\nque oscanfdeixa para trás e faz ofgets"pular", e dois consertos. - O retorno
NULLno 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
fgetslê no máximo 49 caracteres e reserva o último lugar para o'\0'. Use sempresizeofdo 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
fgetsdevolveNULL("nulo") quando não consegue ler nada: a entrada acabou (seção 6). Confira sempre, como você confere o retorno doscanf.
$ 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:
| Índice | 0 | 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 |
|---|---|---|---|---|---|---|---|---|---|---|---|
| Caractere | A | n | a | ␣ | M | a | r | i | a | '\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
strcspne ostrlenmoram 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 denomeaté 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
strcspnconta 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)
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 = "
getsestá 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
getsestourou 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:
- A Ana digitou
29e apertou Enter. Na entrada ficou:29\n. - O
scanf("%d")leu o29e parou antes do\n, que não faz parte do número. O\nficou na entrada. - O
fgetslê "até o Enter". O primeiro caractere que ele encontrou já era o\nque 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 umchar: ogetchardevolve o código do caractere ou o valor especialEOF(end of file, fim da entrada), que não cabe numchar. - (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 umprintfou 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
fgetsdevolveNULLe 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.
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
| Forma | Lê | Limite de tamanho? | Quando usar |
|---|---|---|---|
fgets(s, sizeof s, stdin) | Uma linha, com espaços e o '\n' | Sim | Padrão do guia para qualquer texto digitado. |
scanf("%49s", s) | Uma palavra (para no espaço) | Só se você escrever o número | Quando a resposta é uma palavra só, e com o número. |
scanf("%s", s) | Uma palavra | Não | Nunca: estoura como o gets. |
getchar() | Um caractere | (um por vez) | Ler letra por letra, descartar o resto da linha. |
gets(s) | Uma linha | Não | Nunca. Removido no C11. |
Quando dá errado
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>).
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.
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
| Escrita | O que faz |
|---|---|
fgets(s, sizeof s, stdin) | Lê uma linha (até sizeof s - 1 caracteres), com o '\n', e põe '\0' |
retorno NULL | Nada 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:
2. Tirar o Enter
Complete a linha que tira o '\n' do fim de frase:
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 - 1caracteres, e sempre põe o'\0'. Nunca escreve fora do array.- A linha vem com o
'\n'do Enter. Tire coms[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
getsnão tem limite e foi removido no C11; o gcc 15 nem o reconhece (implicit declaration of function ‘gets’; did you mean ‘fgets’?). - O
scanfdeixa o'\n'na entrada e ofgetsseguinte lê uma linha vazia. Descarte o resto da linha comgetchar, ou leia tudo comfgetse converta comsscanf/strtol. fgetsdevolveNULLno fim da entrada (Ctrl+D no Linux).while (fgets(…) != NULL)lê até o fim.