Última linha duplicada com feof+fgets

Introdução

A leitura de linhas de arquivos é muito comum em programação e o modo mais usual de se fazer isso é usando a função feof como condição de loop e a fgets para a leitura das linhas. Porém, se essas duas funções não forem usadas corretamente, a última linha poderá ser processada duas vezes.

feof+fgets

O modo incorreto de se usar essas duas funções é assim:

[feoferro.c]
#include <stdio.h>

int main() {
    FILE *arq ;
    char buf[BUFSIZ] ;

    arq = fopen("/etc/passwd", "r") ;

    while(!feof(arq)) {
        fgets(buf, BUFSIZ, arq) ;
        printf("%s", buf) ;
    }

    fclose(arq) ;
    return 0 ;
}

O arquivo “/etc/passwd” será aberto para leitura e suas linhas serão armazenadas no buffer buf e, posteriormente, mostradas na tela. Porém, como podemos ver na execução abaixo, a última linha foi processada duas vezes.

$ gcc -o feoferro feoferro.c
$ ./feoferro | tail
nfsnobody:x:65534:65534:Anonymous NFS User:/var/lib/nfs:/sbin/nologin
sshd:x:74:74:Privilege-separated SSH:/var/empty/sshd:/sbin/nologin
backuppc:x:992:988::/var/lib/BackupPC:/sbin/nologin
postgres:x:26:26:PostgreSQL Server:/var/lib/pgsql:/bin/bash
squid:x:23:23::/var/spool/squid:/sbin/nologin
chrony:x:991:987::/var/lib/chrony:/sbin/nologin
distcache:x:94:94:Distcache:/:/sbin/nologin
tcpdump:x:72:72::/:/sbin/nologin
daemonio:x:1000:1000:Marcos Paulo:/home/daemonio:/bin/bash
daemonio:x:1000:1000:Marcos Paulo:/home/daemonio:/bin/bash

O erro

A função feof retorna verdadeiro quando o fim do arquivo é lido e não quando é encontrado [1]. Em outras palavras, feof não verifica se a última linha foi lida e sim se uma leitura após a última linha foi realizada.

O erro da duplicação ocorre porque quando fgets lê a última linha, feof ainda retorna falso, permitindo o loop ser executado novamente. Nesse último passo do loop, fgets realiza uma leitura no fim do arquivo (= EOF, End Of File) sem obter novos dados, e com isso, o conteúdo da última linha no buffer de linhas é usado novamente.

Solução 1

A primeira solução é testar se fgets realizou uma leitura correta verificando seu valor de retorno. Quando EOF é encontrado, essa função retorna NULL.

...
    while(!feof(arq)) {
        if(fgets(buf, BUFSIZ, arq) != NULL) {
            printf("%s", buf) ;
        }
    }
...

O if impede que o buffer seja usado após uma falha da fgets.

Solução 2

Essa solução é a mais usada. Como estamos usando feof e em seguida a fgets, podemos utilizar somente esta última função para o controle do loop.

...
    while(fgets(buf, BUFSIZ, arq) != NULL) {
         printf("%s", buf) ;
    }
...

Usamos esse método quando a primeira coisa que fazemos em um loop é a leitura das linhas. Porém, se rotinas importantes devem ser feitas antes de fgets, opte pela solução 1.

O código correto do fonte apresentado fica assim:

[leituralinhas.c]
#include <stdio.h>

int main() {
    FILE *arq ;
    char buf[BUFSIZ] ;

    arq = fopen("/etc/passwd", "r") ;

    while(fgets(buf, BUFSIZ, arq) != NULL) {
        printf("%s", buf) ;
    }

    fclose(arq) ;
    return 0 ;
}

Usando:

$ gcc -o leituralinhas leituralinhas.c
$ ./leituralinhas | tail
rpcuser:x:29:29:RPC Service User:/var/lib/nfs:/sbin/nologin
nfsnobody:x:65534:65534:Anonymous NFS User:/var/lib/nfs:/sbin/nologin
sshd:x:74:74:Privilege-separated SSH:/var/empty/sshd:/sbin/nologin
backuppc:x:992:988::/var/lib/BackupPC:/sbin/nologin
postgres:x:26:26:PostgreSQL Server:/var/lib/pgsql:/bin/bash
squid:x:23:23::/var/spool/squid:/sbin/nologin
chrony:x:991:987::/var/lib/chrony:/sbin/nologin
distcache:x:94:94:Distcache:/:/sbin/nologin
tcpdump:x:72:72::/:/sbin/nologin
daemonio:x:1000:1000:Marcos Paulo:/home/daemonio:/bin/bash

Referências

[1] Things to Avoid in C/C++ — feof(), Part 3 by WaltP (Acessado em: Outubro/2012)
http://www.gidnetwork.com/b-58.html

2 pensamentos sobre “Última linha duplicada com feof+fgets

Deixe uma resposta

Preencha os seus dados abaixo ou clique em um ícone para log in:

Logotipo do WordPress.com

Você está comentando utilizando sua conta WordPress.com. Sair / Alterar )

Imagem do Twitter

Você está comentando utilizando sua conta Twitter. Sair / Alterar )

Foto do Facebook

Você está comentando utilizando sua conta Facebook. Sair / Alterar )

Foto do Google+

Você está comentando utilizando sua conta Google+. Sair / Alterar )

Conectando a %s