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

Eu vi pessoas tentando ler arquivos como este em muitos posts ultimamente.

Código

#include  #include  int main(int argc, char **argv) { char * path = argc > 1 ? argv[1] : "input.txt"; FILE * fp = fopen(path, "r"); if( fp == NULL ) { perror(path); return EXIT_FAILURE; } while( !feof(fp) ) { /* THIS IS WRONG */ /* Read and process data from file… */ } if( fclose(fp) == 0 ) { return EXIT_SUCCESS; } else { perror(path); return EXIT_FAILURE; } } 

O que há de errado com esse loop while( !feof(fp)) ?

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

concurrency 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 o seu controle. O ambiente realmente existe “concorrentemente” com o seu programa. Como acontece com todas as coisas concorrentes, questões sobre o “estado atual” não fazem sentido: não há conceito de “simultaneidade” entre events concorrentes. Muitas propriedades do estado simplesmente não existem simultaneamente.

Deixe-me tornar isso mais preciso: suponha que você queira perguntar “você tem mais dados”. Você poderia perguntar isso de um contêiner concorrente ou do seu sistema de E / S. Mas a resposta é geralmente inviável e, portanto, sem sentido. Então, se o contêiner diz “sim” – no momento em que você tentar ler, pode não ter mais dados. Da mesma forma, se a resposta for “não”, no momento em que você tentar ler, os dados podem ter chegado. A conclusão é que simplesmente não há propriedade como “Eu tenho dados”, já que você não pode agir de maneira significativa em resposta a qualquer resposta possível. (A situação é um pouco melhor com inputs em buffer, onde você pode conseguir um “sim, eu tenho dados” que constitui algum tipo de garantia, mas você ainda teria que ser capaz de lidar com o caso oposto. é certamente tão ruim quanto eu descrevi: você nunca sabe se esse disco ou esse buffer de rede está cheio.)

Assim, concluímos que é impossível, e de fato não razoável , perguntar a um sistema de E / S se ele será capaz de realizar uma operação de E / S. A única maneira possível de interagirmos com ele (da mesma forma que com um contêiner concorrente) é tentar a operação e verificar se ela foi bem-sucedida ou falhou. No momento em que você interage com o ambiente, só então você pode saber se a interação foi realmente possível e, nesse ponto, deve se comprometer a realizar a interação. (Este é um “ponto de synchronization”, se você quiser.)

EOF

Agora chegamos ao EOF. EOF é a resposta obtida de uma tentativa de operação de E / S. Isso significa que você estava tentando ler ou escrever algo, mas ao fazer isso, você não conseguiu ler ou gravar nenhum dado e, em vez disso, o final da input ou saída foi encontrado. 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ê deve sempre tentar primeiro a operação e depois responder ao sucesso ou falha.

Exemplos

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

  • C stdio, lido de um arquivo:

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

    O resultado que devemos usar é n , o número de elementos que foram lidos (que podem ser tão pouco 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 do scanf , o número de elementos convertidos.

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

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

    O resultado que devemos usar é std::cin , que pode ser avaliado em um contexto booleano e nos diz se o stream ainda está no estado good() .

  • C ++, iostreams getline:

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

    O resultado que devemos usar é novamente std::cin , assim 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 é k , o número de bytes escritos. O ponto aqui é que só podemos saber quantos bytes foram escritos 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 é nbytes , o 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 function retorna explicitamente -1 (e não EOF!) Quando ocorre um erro ou atinge EOF.

Você pode notar que muito raramente soletramos a palavra real "EOF". Nós geralmente detectamos a condição de erro de alguma outra maneira que é mais imediatamente interessante para nós (por exemplo, falha em executar tanto E / S quanto desejávamos). Em cada exemplo, há algum recurso de API que poderia nos dizer explicitamente que o estado EOF foi encontrado, mas na verdade não é uma informação extremamente útil. É muito mais um detalhe do que muitas vezes nos preocupamos. O que importa é se a E / S foi bem-sucedida, mais do que como ela falhou.

  • Um exemplo final que realmente consulta o estado EOF: Suponha que você tenha uma string e queira testar se ela representa um inteiro em sua totalidade, sem nenhum bit extra no final, exceto o espaço em branco. Usando o C ++ iostreams, é 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 } 

    Nós usamos dois resultados aqui. O primeiro é iss , o próprio object de stream, para verificar se a extração formatada para o value bem-sucedida. Porém, depois de também consumir espaço em branco, executamos outra operação de E / S /, iss.get() , e esperamos que ela falhe como EOF, que é o caso se a cadeia inteira já tiver sido consumida pela extração formatada.

    Na biblioteca padrão C, você pode obter algo semelhante com as funções strto*l , verificando se o ponteiro final atingiu o final da string de input.

A resposta

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

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

Considere o seguinte código:

 /* WARNING: demonstration of bad coding technique*/ #include  #include  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! */ (void) 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 consistentemente imprimirá um maior que o número de caracteres no stream de input (assumindo que não há erros de leitura). Considere o caso em que o stream de input está vazio:

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

Nesse caso, feof() é chamado antes de qualquer dado ter sido 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 anulado.

Isso acontece em todos esses casos. feof() não retorna true até que uma leitura no stream encontre o final do arquivo. O objective de feof() é NÃO verificar se a próxima leitura alcançará o final do arquivo. O objective do feof() é distinguir entre um erro de leitura e ter atingido o final do arquivo. Se fread() retornar 0, você deve usar feof / ferror para decidir. Da mesma forma, se fgetc retorna EOF . feof() só é útil depois que o fread retornou zero ou o fgetc retornou o EOF . Antes que isso aconteça, feof() sempre retornará 0.

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

Pior ainda, considere o caso em que ocorre um erro de leitura. Nesse caso, fgetc() retorna EOF , feof() retorna false 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, no mínimo, a condição while deve ser substituída por while(!feof(p) && !ferror(p)) ou há uma possibilidade muito real de um loop infinito, provavelmente expelindo todo tipo de lixo à medida que dados inválidos são processados.

Então, 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 deva haver outra verificação dentro do loop com uma quebra para evitar um loop infinito em um erro de leitura), é quase sempre errado. E mesmo se um caso surgisse onde seria correto, é tão idiomaticamente errado que não seria o jeito certo de escrever o código. Qualquer um que ver esse código deve imediatamente hesitar e dizer "isso é um erro". E possivelmente dar um tapa no autor (a menos que o autor seja seu chefe, caso em que a discrição é aconselhada).

Não, nem sempre é errado. Se sua condição de loop é “enquanto não tentamos ler o último final do arquivo”, você usa while (!feof(f)) . No entanto, esta não é uma condição de loop comum – geralmente você deseja testar outra coisa (como “posso ler mais”). while (!feof(f)) não está errado, é apenas usado errado.

feof () indica se alguém tentou ler após o final do arquivo. Isso significa que tem pouco efeito preditivo: se for verdade, você tem certeza de que a próxima operação de input falhará (você não tem certeza se a anterior falhou BTW), mas se for falsa, você não tem certeza da próxima input operação terá sucesso. Além disso, as operações de input podem falhar por outros motivos que não o final do arquivo (um erro de formato para input formatada, uma falha de IO puro – falha de disco, tempo limite de rede – para todos os tipos de input), mesmo se você puder prever o fim do arquivo (e qualquer um 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 input do próximo linha antes de iniciar o tratamento do anterior), você teria que ser capaz de lidar com uma falha.

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) */ } 

Grande resposta, eu só notei a mesma coisa porque eu estava tentando fazer um loop assim. Então, está errado nesse cenário, mas se você quiser ter um loop que termina normalmente no EOF, essa é uma boa maneira de fazer isso:

 #include  #include  int main(int argc, char *argv[]) { struct stat buf; FILE *fp = fopen(argv[0], "r"); stat(filename, &buf); while (ftello(fp) != buf.st_size) { (void)fgetc(fp); } // all done, read all the bytes }