Por que "while (! Feof (file))" sempre está errado?

573

Ultimamente, tenho visto pessoas tentando ler arquivos como esse em várias postagens:

#include <stdio.h>
#include <stdlib.h>

int
main(int argc, char **argv)
{
    char *path = "stdin";
    FILE *fp = argc > 1 ? fopen(path=argv[1], "r") : stdin;

    if( fp == NULL ) {
        perror(path);
        return EXIT_FAILURE;
    }

    while( !feof(fp) ) {  /* THIS IS WRONG */
        /* Read and process data from file… */
    }
    if( fclose(fp) != 0 ) {
        perror(path);
        return EXIT_FAILURE;
    }
    return EXIT_SUCCESS;
}

O que há de errado com esse loop?

William Pursell
fonte

Respostas:

453

Eu gostaria de fornecer uma perspectiva abstrata e de alto nível.

Concorrência e simultaneidade

As operações de E / S interagem com o ambiente. O ambiente não faz parte do seu programa e não está sob seu controle. O ambiente realmente existe "simultaneamente" com o seu programa. Como acontece com todas as coisas concorrentes, as perguntas sobre o "estado atual" não fazem sentido: não há conceito de "simultaneidade" entre os eventos concorrentes. Muitas propriedades do estado simplesmente não existem simultaneamente.

Deixe-me fazer isso mais preciso: suponha que você queira perguntar: "você tem mais dados". Você pode solicitar isso a um contêiner simultâneo ou ao seu sistema de E / S. Mas a resposta é geralmente impraticável e, portanto, sem sentido. E se o contêiner disser "sim" - quando você tentar ler, ele poderá não ter mais dados. Da mesma forma, se a resposta for "não", no momento em que você tentar ler, os dados poderão ter chegado. A conclusão é que simplesmente existenenhuma propriedade como "Eu tenho dados", pois você não pode agir de maneira significativa em resposta a qualquer resposta possível. (A situação é um pouco melhor com entrada em buffer, onde você pode obter um "sim, eu tenho dados" que constitui algum tipo de garantia, mas você ainda precisa lidar com o caso oposto. E com a saída da situação certamente é tão ruim quanto eu descrevi: você nunca sabe se esse disco ou esse buffer de rede está cheio.)

Portanto, concluímos que é impossível, e de fato não razoável , perguntar a um sistema de E / S se será capaz de executar uma operação de E / S. A única maneira possível de interagir com ele (como em um contêiner simultâneo) é tentar a operação e verificar se foi bem-sucedida ou falhou. Nesse momento em que você interage com o ambiente, então e somente então você pode saber se a interação era realmente possível e, nesse ponto, você deve se comprometer em executar a interação. (Este é um "ponto de sincronização", se você desejar.)

EOF

Agora chegamos ao EOF. EOF é a resposta que você obtém de uma tentativa de operação de E / S. Isso significa que você estava tentando ler ou gravar algo, mas ao fazer isso, não conseguiu ler ou gravar nenhum dado e, em vez disso, foi encontrado o final da entrada ou saída. Isso é verdade para essencialmente todas as APIs de E / S, seja a biblioteca padrão C, iostreams C ++ ou outras bibliotecas. Enquanto as operações de E / S forem bem- sucedidas, você simplesmente não poderá saber se outras operações futuras serão bem-sucedidas. Você sempre deve primeiro tentar a operação e depois responder ao sucesso ou fracasso.

Exemplos

Em cada um dos exemplos, observe com cuidado que primeiro tentamos a operação de E / S e, em seguida, consumimos o resultado, se for válido. Observe ainda que sempre devemos usar o resultado da operação de E / S, embora o resultado tenha diferentes formas e formatos em cada exemplo.

  • C stdio, leia a partir de um arquivo:

    for (;;) {
        size_t n = fread(buf, 1, bufsize, infile);
        consume(buf, n);
        if (n < bufsize) { break; }
    }

    O resultado que devemos usar é no número de elementos que foram lidos (que podem ser tão pequenos quanto zero).

  • C stdio, scanf:

    for (int a, b, c; scanf("%d %d %d", &a, &b, &c) == 3; ) {
        consume(a, b, c);
    }

    O resultado que devemos usar é o valor de retorno de scanf, o número de elementos convertidos.

  • C ++, extração iostreams formatada:

    for (int n; std::cin >> n; ) {
        consume(n);
    }

    O resultado que devemos usar é o std::cinpróprio, que pode ser avaliado em um contexto booleano e informa se o fluxo ainda está no good()estado.

  • C ++, iostreams getline:

    for (std::string line; std::getline(std::cin, line); ) {
        consume(line);
    }

    O resultado que devemos usar é novamente std::cin, exatamente como antes.

  • POSIX, write(2)para liberar um buffer:

    char const * p = buf;
    ssize_t n = bufsize;
    for (ssize_t k = bufsize; (k = write(fd, p, n)) > 0; p += k, n -= k) {}
    if (n != 0) { /* error, failed to write complete buffer */ }

    O resultado que usamos aqui é ko número de bytes gravados. O ponto aqui é que só podemos saber quantos bytes foram gravados após a operação de gravação.

  • POSIX getline()

    char *buffer = NULL;
    size_t bufsiz = 0;
    ssize_t nbytes;
    while ((nbytes = getline(&buffer, &bufsiz, fp)) != -1)
    {
        /* Use nbytes of data in buffer */
    }
    free(buffer);

    O resultado que devemos usar é nbyteso número de bytes até e incluindo a nova linha (ou EOF se o arquivo não terminar com uma nova linha).

    Observe que a função retorna explicitamente -1(e não o EOF!) Quando ocorre um erro ou atinge o EOF.

Você pode notar que raramente escrevemos a palavra "EOF" real. Geralmente, detectamos a condição de erro de alguma outra maneira que é mais imediatamente interessante para nós (por exemplo, falha em executar a quantidade de E / S que desejávamos). Em todos os exemplos, há algum recurso da API que pode nos dizer explicitamente que o estado EOF foi encontrado, mas isso não é realmente uma informação muito útil. É muito mais um detalhe do que geralmente nos preocupamos. O que importa é se a E / S teve êxito, mais do que como falhou.

  • Um exemplo final que realmente consulta o estado EOF: suponha que você tenha uma sequência e queira testar se ela representa um número inteiro na sua totalidade, sem bits extras no final, exceto espaços em branco. Usando C ++ iostreams, fica assim:

    std::string input = "   123   ";   // example
    
    std::istringstream iss(input);
    int value;
    if (iss >> value >> std::ws && iss.get() == EOF) {
        consume(value);
    } else {
        // error, "input" is not parsable as an integer
    }

    Usamos dois resultados aqui. O primeiro é isso próprio objeto de fluxo, para verificar se a extração formatada foi valuebem - sucedida. Porém, depois de consumir também o espaço em branco, executamos outra operação de E / S / iss.get()e esperamos que ela falhe como EOF, o que acontece se toda a cadeia já tiver sido consumida pela extração formatada.

    Na biblioteca padrão C, você pode obter algo semelhante com as strto*lfunções, verificando se o ponteiro final atingiu o final da sequência de entrada.

A resposta

while(!feof)está errado porque testa algo irrelevante e falha ao testar algo que você precisa saber. O resultado é que você está executando um código erroneamente que pressupõe que está acessando dados que foram lidos com êxito, quando na verdade isso nunca aconteceu.

Kerrek SB
fonte
34
@CiaPan: Eu não acho que isso seja verdade. Tanto o C99 quanto o C11 permitem isso.
Kerrek SB
11
Mas o ANSI C não.
CiaPan
3
@ JonathanMee: É ruim por todas as razões que mencionei: você não pode olhar para o futuro. Você não pode dizer o que acontecerá no futuro.
Kerrek SB
3
@JonathanMee: Sim, isso seria apropriado, embora geralmente você possa combinar essa verificação na operação (já que a maioria das operações do iostreams retorna o objeto de fluxo, que por si só tem uma conversão booleana), e dessa maneira você torna óbvio que não está ignorando o valor de retorno.
Kerrek SB /
4
O terceiro parágrafo é notavelmente enganoso / impreciso para uma resposta aceita e altamente votada. feof()não "pergunta ao sistema de E / S se possui mais dados". feof(), de acordo com a página de manual (Linux) : "testa o indicador de fim de arquivo para o fluxo apontado por fluxo, retornando diferente de zero se estiver definido." (também, uma chamada explícita para clearerr()é a única maneira de redefinir esse indicador); A esse respeito, a resposta de William Pursell é muito melhor.
Arne Vogel
234

Está errado porque (na ausência de um erro de leitura) entra no loop mais uma vez do que o autor espera. Se houver um erro de leitura, o loop nunca termina.

Considere o seguinte código:

/* WARNING: demonstration of bad coding technique!! */

#include <stdio.h>
#include <stdlib.h>

FILE *Fopen(const char *path, const char *mode);

int main(int argc, char **argv)
{
    FILE *in;
    unsigned count;

    in = argc > 1 ? Fopen(argv[1], "r") : stdin;
    count = 0;

    /* WARNING: this is a bug */
    while( !feof(in) ) {  /* This is WRONG! */
        fgetc(in);
        count++;
    }
    printf("Number of characters read: %u\n", count);
    return EXIT_SUCCESS;
}

FILE * Fopen(const char *path, const char *mode)
{
    FILE *f = fopen(path, mode);
    if( f == NULL ) {
        perror(path);
        exit(EXIT_FAILURE);
    }
    return f;
}

Este programa sempre imprime um número maior que o número de caracteres no fluxo de entrada (assumindo que não há erros de leitura). Considere o caso em que o fluxo de entrada está vazio:

$ ./a.out < /dev/null
Number of characters read: 1

Nesse caso, feof()é chamado antes que qualquer dado seja lido, portanto, retorna false. O loop é inserido, fgetc()é chamado (e retorna EOF) e a contagem é incrementada. Então feof()é chamado e retorna true, fazendo com que o loop seja cancelado.

Isso acontece em todos esses casos. feof()não retorna verdade até depois de uma leitura sobre o fluxo encontra o final do arquivo. O objetivo de feof()NÃO é verificar se a próxima leitura chegará ao final do arquivo. O objetivo de feof()é distinguir entre um erro de leitura e ter atingido o final do arquivo. Se fread()retornar 0, você deve usar feof/ ferrorpara decidir se um erro foi encontrado ou se todos os dados foram consumidos. Da mesma forma, se fgetcretorna EOF. feof()só é útil depois que o fread retornou zero ou fgetcretornou EOF. Antes que isso aconteça, feof()sempre retornará 0.

É sempre necessário verificar o valor de retorno de uma leitura (uma fread(), ou uma fscanf(), ou uma fgetc()) antes de chamar feof().

Pior ainda, considere o caso em que ocorre um erro de leitura. Nesse caso, fgetc()retorna EOF, feof()retorna falso e o loop nunca termina. Em todos os casos em que while(!feof(p))é usado, deve haver pelo menos uma verificação dentro do loop para ferror(), ou pelo menos a condição while deve ser substituída por while(!feof(p) && !ferror(p))ou existe uma possibilidade muito real de um loop infinito, provavelmente vomitando todo tipo de lixo como dados inválidos estão sendo processados.

Portanto, em resumo, embora eu não possa afirmar com certeza que nunca há uma situação em que possa ser semanticamente correto escrever " while(!feof(f))" (embora seja necessário haver outra verificação dentro do loop com uma pausa para evitar um loop infinito em um erro de leitura ), é quase sempre errado. E mesmo que um caso tenha surgido onde estaria correto, é tão idiomamente errado que não seria o caminho certo para escrever o código. Qualquer pessoa que veja esse código deve hesitar imediatamente e dizer: "isso é um bug". E possivelmente dê um tapa no autor (a menos que o autor seja seu chefe, caso em que a discrição é aconselhável).

William Pursell
fonte
7
Claro que está errado - mas, além disso, não é "horrivelmente feio".
No7
89
Você deve adicionar um exemplo de código correto, pois imagino que muitas pessoas virão aqui procurando uma solução rápida.
Jleahy
6
@ Thomas: Eu não sou um especialista em C ++, mas acredito que file.eof () retorna efetivamente o mesmo resultado que feof(file) || ferror(file), portanto, é muito diferente. Mas esta questão não se destina a ser aplicável ao C ++.
precisa saber é o seguinte
6
@ m-ric também não está certo, porque você ainda tentará processar uma leitura que falhou.
Mark Ransom
4
esta é a resposta correta real. feof () é usado para saber o resultado da tentativa de leitura anterior. Portanto, provavelmente você não deseja usá-lo como sua condição de quebra de loop. 1
Jack
63

Não, nem sempre é errado. Se sua condição de loop for "enquanto não tentamos ler o final do arquivo passado", você o utilizará while (!feof(f)). No entanto, essa não é uma condição comum de loop - geralmente você deseja testar outra coisa (como "posso ler mais"). while (!feof(f))não está errado, é apenas usado errado.

Erik
fonte
1
Gostaria de saber ... f = fopen("A:\\bigfile"); while (!feof(f)) { /* remove diskette */ }ou (indo para testar isso) #f = fopen(NETWORK_FILE); while (!feof(f)) { /* unplug network cable */ }
005 pm
1
@pmg: Como dito, "não é uma condição comum de loop" hehe. Eu realmente não consigo pensar em nenhum caso em que precisei, geralmente estou interessado em "eu poderia ler o que eu queria" com tudo o que implica no tratamento de erros?
Erik
@pmg: Como disse, você raramente queremwhile(!eof(f))
Erik
9
Mais precisamente, a condição é "enquanto não tentamos ler além do final do arquivo e não houve erro de leitura" feofnão se trata de detectar o final do arquivo; trata-se de determinar se uma leitura foi curta devido a um erro ou porque a entrada está esgotada.
William Pursell
35

feof()indica se alguém tentou ler após o final do arquivo. Isso significa que ele tem pouco efeito preditivo: se for verdade, você tem certeza de que a próxima operação de entrada falhará (você não tem certeza de que a anterior falhou no BTW), mas se for falsa, não terá certeza da próxima entrada operação terá sucesso. Além disso, as operações de entrada podem falhar por outros motivos que não o final do arquivo (um erro de formato para entrada formatada, uma falha de E / S pura - falha de disco, tempo limite da rede - para todos os tipos de entrada), portanto, mesmo que você possa prever o final do arquivo (e qualquer pessoa que tenha tentado implementar o Ada one, que é preditivo, dirá que pode ser complexo se você precisar pular espaços e que tem efeitos indesejáveis ​​em dispositivos interativos - às vezes forçando a entrada do próximo linha antes de iniciar o manuseio da anterior),

Portanto, o idioma correto em C é fazer um loop com o sucesso da operação de E / S como condição de loop e, em seguida, testar a causa da falha. Por exemplo:

while (fgets(line, sizeof(line), file)) {
    /* note that fgets don't strip the terminating \n, checking its
       presence allow to handle lines longer that sizeof(line), not showed here */
    ...
}
if (ferror(file)) {
   /* IO failure */
} else if (feof(file)) {
   /* format error (not possible with fgets, but would be with fscanf) or end of file */
} else {
   /* format error (not possible with fgets, but would be with fscanf) */
}
AProgrammer
fonte
2
Chegar ao final de um arquivo não é um erro, por isso questiono o fraseado "operações de entrada podem falhar por outros motivos que não o final do arquivo".
William Pursell
@WilliamPursell, alcançar o eof não é necessariamente um erro, mas ser incapaz de executar uma operação de entrada por causa do eof é um. E é impossível em C detectar de forma confiável a Eof sem ter feito uma operação de entrada falhar.
AProgrammer 29/09/12
Concordar por último elsenão é possível com sizeof(line) >= 2e fgets(line, sizeof(line), file)mas possível com patológico size <= 0e fgets(line, size, file). Talvez até possível com sizeof(line) == 1.
chux - Restabelece Monica 26/03
1
Toda essa conversa sobre "valor preditivo" ... nunca pensei nisso dessa maneira. No meu mundo, feof(f)não prediz nada. Ele afirma que uma operação ANTERIOR atingiu o final do arquivo. Nada mais nada menos. E se não houve operação anterior (apenas a abriu), ela não informa o final do arquivo, mesmo que o arquivo estivesse vazio para começar. Portanto, além da explicação da simultaneidade em outra resposta acima, não creio que exista qualquer razão para não fazer o loop feof(f).
BitTickler 24/09
@AProgrammer: A "ler até N bytes" pedido que os rendimentos zero, seja por causa de uma "permanente" EOF ou porque está disponível há mais dados ainda , não é um erro. Embora feof () possa não prever com segurança que solicitações futuras renderão dados, ele pode indicar com segurança que solicitações futuras não . Talvez deva haver uma função de status que indique "É plausível que futuras solicitações de leitura sejam bem-sucedidas", com semântica que após a leitura até o final de um arquivo comum, uma implementação de qualidade deve dizer que é improvável que as futuras leituras tenham sucesso, sem algum motivo para acredito que eles podem .
Supercat
0

feof()não é muito intuitivo. Na minha humilde opinião, o estado FILEde final de arquivo deve ser definido como truese qualquer operação de leitura resultar no final do arquivo. Em vez disso, você deve verificar manualmente se o fim do arquivo foi atingido após cada operação de leitura. Por exemplo, algo assim funcionará se estiver lendo um arquivo de texto usando fgetc():

#include <stdio.h>

int main(int argc, char *argv[])
{
  FILE *in = fopen("testfile.txt", "r");

  while(1) {
    char c = fgetc(in);
    if (feof(in)) break;
    printf("%c", c);
  }

  fclose(in);
  return 0;
}

Seria ótimo se algo assim funcionasse:

#include <stdio.h>

int main(int argc, char *argv[])
{
  FILE *in = fopen("testfile.txt", "r");

  while(!feof(in)) {
    printf("%c", fgetc(in));
  }

  fclose(in);
  return 0;
}
Scott Deagan
fonte
1
printf("%c", fgetc(in));? Esse é um comportamento indefinido. fgetc()retorna int, não char.
Andrew Henle
Parece-me que o idioma padrão while( (c = getchar()) != EOF)é muito "algo assim".
William Pursell
while( (c = getchar()) != EOF)funciona em um dos meus desktops executando o GNU C 10.1.0, mas falha no meu Raspberry Pi 4 executando o GNU C 9.3.0. No meu RPi4, ele não detecta o final do arquivo e continua.
Scott Deagan
@AndrewHenle Você está certo! Mudando char cpara int cobras! Obrigado!!
Scott Deagan