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)).

Você vai aprender
  • Ler um caractere por vez com fgetc, e por que a variável precisa ser int, e não char.
  • Ler uma linha por vez com fgets, tirar o \n e 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çãoLêDevolve no fim do arquivoBom para
fgetc(arq)Um caractere (um byte)EOFContar, copiar, olhar byte a byte
fgets(linha, tam, arq)Uma linha, até tam - 1 bytesNULLQuase tudo: textos, listas, CSV
fscanf(arq, formato, …)Palavras e números, como o scanfEOFArquivos 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ão char. O fgetc precisa devolver 256 valores de byte (0 a 255) e mais um, o EOF, que vale -1. Um char nã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 do stdio.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. Devolve linha se leu alguma coisa, ou NULL se 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 \n aqui é do printf, 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, &nota) == 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, &nota)
Pula espaços e quebras de linha, lê uma palavra (até 49 letras, para caber em nome[50]) e depois um número double (%lf). O nome não leva & porque é um array; a nota leva.
== 2
O fscanf devolve quantos valores conseguiu preencher. Pedimos dois; só seguimos se vieram os dois. No fim do arquivo ele devolve EOF, 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, &nota) == 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óximo fgets.
%49[^;]
Um formato especial: "leia até 49 caracteres que não sejam ;". Diferente do %s, ele aceita espaços, então Caneta azul entra 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). A linha ainda tem o '\n' do fim, por isso esse printf nã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.

feof responde sobre o passado

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…UseLaço
Contar, procurar ou copiar caracteresfgetcwhile ((c = fgetc(arq)) != EOF), com int c
Mostrar ou processar um texto linha por linhafgetswhile (fgets(linha, sizeof linha, arq) != NULL)
Ler dados (CSV, listas, configuração)fgets + sscanfO fgets no laço; o sscanf dentro, conferindo o retorno
Um arquivo pequeno, gerado pelo seu próprio programafscanfwhile (fscanf(arq, …) == N)
Os bytes da memória (arquivo binário)freadAula Arquivos Binários

Quando dá errado

A última linha aparece duas vezes: 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.

O programa para no meio do arquivo: 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.

Dados trocados: o retorno do fscanf não foi conferido
    while (fscanf(arq, "%49s %lf", nome, &nota) != 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).

Nomes cortados: %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

EscritaO 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, &nota)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:

c = fgetc(arq);

2. O fim do fgets

Complete a condição do laço que lê o arquivo linha por linha:

while (fgets(linha, sizeof linha, arq) != ) { … }

3. Quantos valores?

Complete para o laço continuar só quando o fscanf conseguir ler o nome e a idade:

while (fscanf(arq, "%49s %d", nome, &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, fgetc e fscanf devolvem EOF, e fgets devolve NULL.
  • fgetc lê um byte; guarde em int c, nunca em char. Acentos em UTF-8 são 2 bytes.
  • fgets lê uma linha com o \n e nunca passa do tamanho do array; uma linha grande vem em pedaços.
  • fscanf lê 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 fgets e separe com sscanf: a linha ruim é avisada e a leitura continua.
  • Ponha a leitura na condição do laço. feof e ferror servem depois do laço, para saber por que ele parou; while (!feof(arq)) repete a última linha.