C Ler Arquivos
Na aula Arquivos, você aprendeu a abrir e fechar. Agora vem o recheio: ler o que tem dentro. O C oferece três jeitos, como três jeitos de ler um livro: letra por letra (fgetc), linha por linha (fgets) e procurando palavras e números (fscanf). Nesta aula você vai usar os três com a lista de notas e o estoque da lojinha da Ana, descobrir como o programa percebe que o arquivo acabou e conhecer o erro mais famoso da leitura de arquivos, o while (!feof(arq)).
- Ler um caractere por vez com
fgetc, e por que a variável precisa serint, e nãochar. - Ler uma linha por vez com
fgets, tirar o\ne lidar com linhas maiores que o array. - Ler palavras e números com
fscanf, conferindo o que ele devolve. - O jeito mais seguro de ler dados:
fgets+sscanf, uma linha por vez. - Como saber se o arquivo acabou (
EOF,feof) ou se deu erro (ferror).
Todos os programas desta aula 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. Os primeiros exemplos leem o arquivo poema.txt:
$ cat poema.txt Batatinha quando nasce espalha a rama pelo chão. Menininha quando dorme põe a mão no coração.
A ideia: o dedo que corre pela página
Quando você lê um livro em voz alta, seu dedo vai marcando onde você está. A cada palavra lida, o dedo anda. Quando chega ao fim da última página, acabou: não adianta pedir mais.
O arquivo aberto funciona assim. O FILE * guarda a posição (o dedo), que começa no primeiro byte. Toda leitura entrega o que está na posição e avança. Quando não há mais nada, as funções avisam com um valor especial: EOF (de end of file, fim de arquivo) ou NULL, conforme a função.
| Função | Lê | Devolve no fim do arquivo | Bom para |
|---|---|---|---|
fgetc(arq) | Um caractere (um byte) | EOF | Contar, copiar, olhar byte a byte |
fgets(linha, tam, arq) | Uma linha, até tam - 1 bytes | NULL | Quase tudo: textos, listas, CSV |
fscanf(arq, formato, …) | Palavras e números, como o scanf | EOF | Arquivos simples e bem comportados |
1. Letra por letra: fgetc
O fgetc ("file get character", pegar um caractere do arquivo) devolve o próximo byte do arquivo. Este programa conta quantos bytes e quantas linhas o poema tem:
#include <stdio.h>
int main(void) {
FILE *arq = fopen("poema.txt", "r");
if (arq == NULL) {
perror("poema.txt");
return 1;
}
int c;
long bytes = 0, linhas = 0;
while ((c = fgetc(arq)) != EOF) {
bytes++;
if (c == '\n') {
linhas++;
}
}
fclose(arq);
printf("%ld bytes, %ld linhas\n", bytes, linhas);
return 0;
}
- int c;
- A variável que recebe cada caractere é
int, e nãochar. Ofgetcprecisa devolver 256 valores de byte (0 a 255) e mais um, oEOF, que vale -1. Umcharnão tem espaço para os 257 (veja "Quando dá errado"). - (c = fgetc(arq))
- Lê um byte, guarda em
c, e a posição do arquivo anda uma casa. Os parênteses em volta são obrigatórios: sem eles, o!=seria feito antes do=(aula Precedência). - != EOF
- Enquanto não chegou ao fim, o laço continua.
EOFé uma constante dostdio.h. - if (c == '\n')
- Cada
'\n'termina uma linha: contar os'\n'é contar as linhas.
$ gcc -std=c23 -Wall -Wextra letras.c -o letras $ ./letras 99 bytes, 4 linhas $ wc -c -l poema.txt « o comando wc conta igual 4 99 poema.txt
99 bytes, mas o poema tem só 94 caracteres (contando as 4 quebras de linha): cada letra acentuada (ã, õ, ç) ocupa 2 bytes em UTF-8, e o fgetc lê bytes, não letras (aula Acentos e UTF-8). O comando wc (de word count) do terminal confirma as contas.
Missão: a caixa tem o programa e o arquivo poema.txt numa aba. Conte também os espaços: crie a variável espacos, some 1 a cada ' ' e mostre no fim, para a saída ficar 99 bytes, 4 linhas, 12 espaços.
#include <stdio.h>
int main(void) {
FILE *arq = fopen("poema.txt", "r");
if (arq == NULL) {
perror("poema.txt");
return 1;
}
int c;
long bytes = 0, linhas = 0;
while ((c = fgetc(arq)) != EOF) {
bytes++;
if (c == '\n') {
linhas++;
}
}
fclose(arq);
printf("%ld bytes, %ld linhas\n", bytes, linhas);
return 0;
}
Batatinha quando nasce espalha a rama pelo chão. Menininha quando dorme põe a mão no coração.
#include <stdio.h>
int main(void) {
FILE *arq = fopen("poema.txt", "r");
if (arq == NULL) {
perror("poema.txt");
return 1;
}
int c;
long bytes = 0, linhas = 0, espacos = 0;
while ((c = fgetc(arq)) != EOF) {
bytes++;
if (c == '\n') {
linhas++;
}
if (c == ' ') {
espacos++;
}
}
fclose(arq);
printf("%ld bytes, %ld linhas, %ld espaços\n", bytes, linhas, espacos);
return 0;
}
A linha em branco no fim da aba poema.txt está ali de propósito: é o \n que termina a última linha, como no arquivo da Ana.
2. Linha por linha: fgets
Ler letra por letra é bom para contar. Para trabalhar com o conteúdo, quase sempre você quer uma linha inteira. O fgets ("file get string") é o mesmo que você usou para ler do teclado na aula Ler uma Linha; só troca o stdin pelo arquivo:
#include <stdio.h>
#include <string.h>
int main(void) {
FILE *arq = fopen("poema.txt", "r");
if (arq == NULL) {
perror("poema.txt");
return 1;
}
char linha[100];
int n = 0;
while (fgets(linha, sizeof linha, arq) != NULL) {
linha[strcspn(linha, "\n")] = '\0';
n++;
printf("%2d | %s\n", n, linha);
}
fclose(arq);
return 0;
}
- char linha[100];
- O "balde" onde cada linha é despejada: até 99 bytes mais o
'\0'. - fgets(linha, sizeof linha, arq)
- Lê do arquivo até encontrar um
'\n'(que vem junto) ou até encher o balde, o que acontecer primeiro. Devolvelinhase leu alguma coisa, ouNULLse o arquivo já tinha acabado. - != NULL
- O laço para no fim do arquivo. É o jeito certo de escrever esse laço.
- linha[strcspn(linha, "\n")] = '\0';
- Corta o
'\n'do fim, trocando-o por'\0', o mesmo truque da aula fgets. Se a linha não tem'\n', não muda nada. - "%2d | %s\n"
- O número da linha com 2 casas e o texto. O
\naqui é doprintf, já que o da linha foi cortado.
$ gcc -std=c23 -Wall -Wextra numerar.c -o numerar $ ./numerar 1 | Batatinha quando nasce 2 | espalha a rama pelo chão. 3 | Menininha quando dorme 4 | põe a mão no coração.
Linha maior que o balde
O que acontece se a linha não cabe no array? Com char linha[16], cabem 15 bytes por vez:
char linha[16];
while (fgets(linha, sizeof linha, arq) != NULL) {
printf("[%s]\n", linha);
}
$ gcc -std=c23 -Wall -Wextra pedacos.c -o pedacos $ ./pedacos [Batatinha quand] [o nasce ] [espalha a rama ] [pelo chão. ] [Menininha quand] [o dorme ] [põe a mão no ] [coração. ]
Nada se perde e nada transborda: o fgets nunca escreve além dos 16 bytes. Ele entrega a linha em pedaços, e o pedaço que termina com '\n' (repare no ] que foi para a linha de baixo) é o último daquela linha. O primeiro pedaço da quarta linha tem só 13 caracteres (põe a mão no ) porque õ e ã ocupam 2 bytes cada. Para os seus programas, a lição é: use um array folgado (algumas centenas de bytes, para um texto comum) e, se a linha puder ser maior, confira se o pedaço termina com '\n'.
3. Palavras e números: fscanf
A Ana guarda as notas da turma em notas.txt, um aluno por linha, com o nome e a nota separados por espaço:
$ cat notas.txt Ana 8.5 Bruno 6.0 Carla 9.5 Davi 7.0
O fscanf ("file scan formatted") é o scanf dos arquivos: o mesmo formato, com o arquivo como primeiro argumento.
#include <stdio.h>
int main(void) {
FILE *arq = fopen("notas.txt", "r");
if (arq == NULL) {
perror("notas.txt");
return 1;
}
char nome[50];
double nota;
double soma = 0.0;
int n = 0;
while (fscanf(arq, "%49s %lf", nome, ¬a) == 2) {
printf("%-6s %4.1f\n", nome, nota);
soma += nota;
n++;
}
fclose(arq);
if (n > 0) {
printf("Média de %d alunos: %.2f\n", n, soma / n);
}
return 0;
}
- fscanf(arq, "%49s %lf", nome, ¬a)
- Pula espaços e quebras de linha, lê uma palavra (até 49 letras, para caber em
nome[50]) e depois um númerodouble(%lf). Onomenão leva&porque é um array; anotaleva. - == 2
- O
fscanfdevolve quantos valores conseguiu preencher. Pedimos dois; só seguimos se vieram os dois. No fim do arquivo ele devolveEOF, que também é diferente de 2, e o laço para. - if (n > 0)
- Evita dividir por zero se o arquivo estiver vazio.
$ gcc -std=c23 -Wall -Wextra notas.c -o notas $ ./notas Ana 8.5 Bruno 6.0 Carla 9.5 Davi 7.0 Média de 4 alunos: 7.75
Quando o arquivo não está perfeito
Arquivos escritos por pessoas têm erros. Na notas2.txt, alguém digitou Carla oito em vez de Carla 9.5. O mesmo programa, lendo esse arquivo:
$ cat notas2.txt Ana 8.5 Bruno 6.0 Carla oito Davi 7.0 $ ./notas2 Ana 8.5 Bruno 6.0 Média de 2 alunos: 7.25
Em Carla oito, o %lf não conseguiu ler um número, o fscanf devolveu 1, e o laço parou ali. O programa não travou nem mostrou lixo, porque conferiu o == 2, mas o Davi ficou de fora, e nada avisou o usuário. E o fscanf não diz em que linha estava: para ele, o arquivo é uma sequência de palavras, e as quebras de linha são só mais espaços. Por isso, para dados de verdade, existe um jeito melhor.
Experimente: rode com o notas.txt da aba. Depois mude o arquivo: troque a nota da Carla por oito; ponha duas notas na mesma linha (Ana 8.5 Bruno 6.0); escreva Ana Maria 8.5. Para cada caso, adivinhe a saída antes de rodar.
#include <stdio.h>
int main(void) {
FILE *arq = fopen("notas.txt", "r");
if (arq == NULL) {
perror("notas.txt");
return 1;
}
char nome[50];
double nota;
double soma = 0.0;
int n = 0;
while (fscanf(arq, "%49s %lf", nome, ¬a) == 2) {
printf("%-6s %4.1f\n", nome, nota);
soma += nota;
n++;
}
fclose(arq);
if (n > 0) {
printf("Média de %d alunos: %.2f\n", n, soma / n);
}
return 0;
}
Ana 8.5 Bruno 6.0 Carla 9.5 Davi 7.0
4. O jeito seguro: fgets + sscanf
A solução usada em programas de verdade junta o melhor dos dois: o fgets lê uma linha inteira (assim você sabe o número da linha e uma linha ruim nunca contamina a seguinte), e o sscanf ("string scan formatted") separa os valores de dentro da linha, como um fscanf que lê de uma string (aula Texto ↔ Número).
O estoque da Ana está num arquivo CSV (comma-separated values, valores separados por vírgula; no Brasil, as planilhas costumam usar ponto e vírgula). Cada linha tem nome, quantidade e preço. Os nomes têm espaços, e uma linha tem um erro de digitação:
$ cat estoque.csv Caderno;12;15.90 Caneta azul;40;2.50 Borracha;vinte;1.75 Cola branca;8;4.25
#include <stdio.h>
int main(void) {
FILE *arq = fopen("estoque.csv", "r");
if (arq == NULL) {
perror("estoque.csv");
return 1;
}
char linha[200];
int num = 0;
double total = 0.0;
while (fgets(linha, sizeof linha, arq) != NULL) {
num++;
char nome[50];
int qtd;
double preco;
if (sscanf(linha, "%49[^;];%d;%lf", nome, &qtd, &preco) != 3) {
printf("Linha %d ignorada: %s", num, linha);
continue;
}
printf("%-14s %3d x R$ %6.2f\n", nome, qtd, preco);
total += qtd * preco;
}
fclose(arq);
printf("Valor do estoque: R$ %.2f\n", total);
return 0;
}
- fgets(linha, sizeof linha, arq)
- Uma linha do arquivo por volta do laço, como na seção 2.
- num++
- Conta as linhas, para a mensagem de erro dizer onde está o problema.
- sscanf(linha, …)
- Lê da string
linha, e não do arquivo. Se der errado, só esta linha é perdida: a próxima volta do laço começa limpa, com o próximofgets. - %49[^;]
- Um formato especial: "leia até 49 caracteres que não sejam
;". Diferente do%s, ele aceita espaços, entãoCaneta azulentra inteiro. - ;%d;%lf
- Um
;no formato precisa aparecer na linha. Depois vêm a quantidade (int), outro;e o preço (double). - != 3
- Três valores pedidos, três esperados. Se não, avisa e pula para a próxima linha com
continue(aula break e continue). Alinhaainda tem o'\n'do fim, por isso esseprintfnão precisa de outro.
$ gcc -std=c23 -Wall -Wextra estoque.c -o estoque $ ./estoque Caderno 12 x R$ 15.90 Caneta azul 40 x R$ 2.50 Linha 3 ignorada: Borracha;vinte;1.75 Cola branca 8 x R$ 4.25 Valor do estoque: R$ 324.80
Compare com o fscanf da seção 3: a linha ruim foi avisada, com o número, e a leitura continuou na linha seguinte. A Cola branca entrou na conta.
5. O arquivo acabou ou deu erro? feof e ferror
Quando uma leitura devolve EOF ou NULL, há dois motivos possíveis: o arquivo acabou (o normal) ou aconteceu um erro no meio (um pendrive arrancado, um disco com defeito). Depois do laço, duas funções dizem qual foi:
feof(arq)("end of file"): verdadeiro se a leitura parou porque o arquivo acabou;ferror(arq)("file error"): verdadeiro se a leitura parou por um erro.
#include <stdio.h>
int main(void) {
FILE *arq = fopen("poema.txt", "r");
if (arq == NULL) {
perror("poema.txt");
return 1;
}
char linha[100];
int n = 0;
while (fgets(linha, sizeof linha, arq) != NULL) {
n++;
}
if (ferror(arq)) {
printf("Erro de leitura depois de %d linhas!\n", n);
} else if (feof(arq)) {
printf("Cheguei ao fim: %d linhas.\n", n);
}
fclose(arq);
return 0;
}
$ gcc -std=c23 -Wall -Wextra fimerro.c -o fimerro $ ./fimerro Cheguei ao fim: 4 linhas.
Um erro de leitura é raro, mas dá para ver um. No Linux, o fopen consegue abrir uma pasta com "r", mas a primeira leitura falha. A Ana trocou o nome do arquivo por "relatorios", uma pasta, e pôs um perror depois do laço:
FILE *arq = fopen("relatorios", "r");
/* … o mesmo if (arq == NULL) … */
char linha[100];
while (fgets(linha, sizeof linha, arq) != NULL) {
printf("%s", linha);
}
if (ferror(arq)) {
perror("fgets");
}
$ ./pasta_ler fgets: Is a directory
O fopen deu certo, o fgets devolveu NULL logo na primeira vez, e o ferror revelou que não era fim de arquivo: era um erro, "é uma pasta". Sem o ferror, o programa acharia que leu um arquivo vazio.
O feof não "olha adiante" para ver se ainda há algo para ler. Ele só fica verdadeiro depois que uma leitura tentou passar do fim e falhou. Por isso ele serve para depois do laço, e não para a condição do laço. O erro clássico de usá-lo no while está logo abaixo.
Formas de ler: qual usar
| Você quer… | Use | Laço |
|---|---|---|
| Contar, procurar ou copiar caracteres | fgetc | while ((c = fgetc(arq)) != EOF), com int c |
| Mostrar ou processar um texto linha por linha | fgets | while (fgets(linha, sizeof linha, arq) != NULL) |
| Ler dados (CSV, listas, configuração) | fgets + sscanf | O fgets no laço; o sscanf dentro, conferindo o retorno |
| Um arquivo pequeno, gerado pelo seu próprio programa | fscanf | while (fscanf(arq, …) == N) |
| Os bytes da memória (arquivo binário) | fread | Aula Arquivos Binários |
Quando dá errado
while (!feof(arq))
char linha[100];
while (!feof(arq)) {
fgets(linha, sizeof linha, arq);
printf("> %s", linha);
}
$ gcc -std=c23 -Wall -Wextra feof.c -o feof $ ./feof > Batatinha quando nasce > espalha a rama pelo chão. > Menininha quando dorme > põe a mão no coração. > põe a mão no coração. « repetida!
O que aconteceu: depois de ler a 4ª linha, o arquivo está no fim, mas o feof ainda é falso, porque nenhuma leitura tentou passar dele (seção 5). O laço dá mais uma volta, o fgets falha e devolve NULL sem mexer em linha, e o printf mostra a linha antiga de novo. Nenhum compilador avisa. No editor do guia sai igual.
Como resolver: ponha a própria leitura na condição: while (fgets(linha, sizeof linha, arq) != NULL). Deixe o feof para depois do laço.
char c com fgetc
char c;
int bytes = 0;
while ((c = fgetc(arq)) != EOF) {
bytes++;
}
printf("Li %d bytes\n", bytes);
$ printf 'abc\377def' > dados.bin « 7 bytes; o do meio vale 255 $ xxd dados.bin 00000000: 6162 63ff 6465 66 abc.def $ gcc -std=c23 -Wall -Wextra charerrado.c -o charerrado $ ./charerrado Li 3 bytes
O que aconteceu: o arquivo tem 7 bytes, mas o programa parou no 4º, o byte ff (255). Guardado num char, que no Linux vai de -128 a 127, o 255 vira -1, que é o valor de EOF: o laço achou que o arquivo tinha acabado. Em computadores em que char não tem sinal (muitos ARM), acontece o contrário: o EOF nunca aparece e o laço não termina. Nenhum aviso, nem com -Wall -Wextra.
Como resolver: a variável do fgetc é sempre int c. Depois de conferir que não é EOF, você pode usá-la como caractere à vontade.
fscanf não foi conferido
while (fscanf(arq, "%49s %lf", nome, ¬a) != EOF) {
printf("%s: %.1f\n", nome, nota);
}
$ ./travou Ana: 8.5 Bruno: 6.0 Carla: 6.0 « a nota do Bruno, que sobrou na variável oito: 6.0 « "oito" lido como nome Davi: 7.0
O que aconteceu: com o notas2.txt (o da Carla oito), o laço só parava no EOF. Quando o %lf falhou em oito, o fscanf devolveu 1 (só o nome), o programa usou a nota velha, e na volta seguinte leu oito como se fosse um nome. Com outros arquivos, um laço assim pode até ficar preso para sempre, tentando ler o mesmo texto que não é número.
Como resolver: compare com a quantidade de valores pedidos (== 2), ou use fgets + sscanf (seção 4).
%s para no espaço
O que aconteceu: com fscanf(arq, "%49s %lf", …), uma linha Ana Maria 8.5 dá o nome Ana, e o %lf tenta ler Maria como número e falha. O %s lê uma palavra, e não uma frase.
Como resolver: use um separador que não aparece nos nomes (Ana Maria;8.5) e leia com %49[^;], como no estoque.c.
Falha de segmentação logo na primeira leitura
O que aconteceu: o fopen devolveu NULL (o arquivo não existe ou o nome está errado) e o programa leu mesmo assim. É o primeiro cenário da aula Arquivos.
Como resolver: if (arq == NULL) { perror(nome); return 1; } logo depois do fopen.
Referência rápida
| Escrita | O que faz |
|---|---|
int c = fgetc(arq); | Lê um byte; EOF no fim. A variável é int. |
while ((c = fgetc(arq)) != EOF) | Lê o arquivo inteiro, um byte por vez. |
fgets(linha, sizeof linha, arq) | Lê uma linha (com o \n), no máximo sizeof linha - 1 bytes; NULL no fim. |
linha[strcspn(linha, "\n")] = '\0'; | Tira o \n do fim da linha. |
fscanf(arq, "%49s %lf", nome, ¬a) | Lê uma palavra e um número; devolve quantos leu, ou EOF. |
sscanf(linha, "%49[^;];%d", nome, &qtd) | Separa os valores de uma linha já lida. |
%49[^;] | Lê até 49 caracteres que não sejam ; (aceita espaços). |
feof(arq) | Depois do laço: a leitura parou porque o arquivo acabou? |
ferror(arq) | Depois do laço: a leitura parou por um erro? |
Exercícios
1. O tipo certo
Complete o tipo da variável que recebe o fgetc:
2. O fim do fgets
Complete a condição do laço que lê o arquivo linha por linha:
3. Quantos valores?
Complete para o laço continuar só quando o fscanf conseguir ler o nome e a idade:
4. Um arquivo tem 3 linhas, e o programa usa while (!feof(arq)) { fgets(…); printf(…); }. Quantas linhas aparecem na tela?
5. Com char linha[10], quantos bytes do arquivo o fgets(linha, sizeof linha, arq) lê, no máximo, de uma vez?
6. Por que fgets + sscanf é melhor que fscanf para ler um CSV?
Missão final: o estoque da lojinha
Missão: o programa já lê o estoque.csv (na outra aba) linha por linha, mas não separa os valores. Dentro do laço, use sscanf para tirar da linha o nome, a quantidade e o preço. Se não vierem os 3 valores, mostre Linha N ignorada: … com o número da linha e pule para a próxima com continue. A saída deve ser:
Caderno 12 x R$ 15.90 Caneta azul 40 x R$ 2.50 Linha 3 ignorada: Borracha;vinte;1.75 Cola branca 8 x R$ 4.25 Valor do estoque: R$ 324.80
O aviso de num "não usada" some quando você usar o número da linha na mensagem.
#include <stdio.h>
int main(void) {
FILE *arq = fopen("estoque.csv", "r");
if (arq == NULL) {
perror("estoque.csv");
return 1;
}
char linha[200];
int num = 0;
double total = 0.0;
while (fgets(linha, sizeof linha, arq) != NULL) {
num++;
char nome[50] = "";
int qtd = 0;
double preco = 0.0;
/* separe nome, qtd e preco da linha com sscanf;
se não vierem os 3, avise e pule a linha */
printf("%-14s %3d x R$ %6.2f\n", nome, qtd, preco);
total += qtd * preco;
}
fclose(arq);
printf("Valor do estoque: R$ %.2f\n", total);
return 0;
}
Caderno;12;15.90 Caneta azul;40;2.50 Borracha;vinte;1.75 Cola branca;8;4.25
#include <stdio.h>
int main(void) {
FILE *arq = fopen("estoque.csv", "r");
if (arq == NULL) {
perror("estoque.csv");
return 1;
}
char linha[200];
int num = 0;
double total = 0.0;
while (fgets(linha, sizeof linha, arq) != NULL) {
num++;
char nome[50] = "";
int qtd = 0;
double preco = 0.0;
if (sscanf(linha, "%49[^;];%d;%lf", nome, &qtd, &preco) != 3) {
printf("Linha %d ignorada: %s", num, linha);
continue;
}
printf("%-14s %3d x R$ %6.2f\n", nome, qtd, preco);
total += qtd * preco;
}
fclose(arq);
printf("Valor do estoque: R$ %.2f\n", total);
return 0;
}
Se quiser ir além: acrescente ao estoque.csv uma linha sem o preço (Régua;5) e outra em branco, e veja as duas serem avisadas. Depois mostre também quantas linhas foram ignoradas no total.
Resumo
- Toda leitura entrega o que está na posição atual e avança. No fim do arquivo,
fgetcefscanfdevolvemEOF, efgetsdevolveNULL. fgetclê um byte; guarde emint c, nunca emchar. Acentos em UTF-8 são 2 bytes.fgetslê uma linha com o\ne nunca passa do tamanho do array; uma linha grande vem em pedaços.fscanflê palavras e números; confira se ele devolveu a quantidade pedida (== 2), e não só!= EOF.- Para dados de verdade, leia a linha com
fgetse separe comsscanf: a linha ruim é avisada e a leitura continua. - Ponha a leitura na condição do laço.
feofeferrorservem depois do laço, para saber por que ele parou;while (!feof(arq))repete a última linha.