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?
feof()
para controlar um loopRespostas:
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:
O resultado que devemos usar é
n
o número de elementos que foram lidos (que podem ser tão pequenos quanto zero).C stdio,
scanf
:O resultado que devemos usar é o valor de retorno de
scanf
, o número de elementos convertidos.C ++, extração iostreams formatada:
O resultado que devemos usar é o
std::cin
próprio, que pode ser avaliado em um contexto booleano e informa se o fluxo ainda está nogood()
estado.C ++, iostreams getline:
O resultado que devemos usar é novamente
std::cin
, exatamente como antes.POSIX,
write(2)
para liberar um buffer:O resultado que usamos aqui é
k
o 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()
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 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:
Usamos dois resultados aqui. O primeiro é
iss
o próprio objeto de fluxo, para verificar se a extração formatada foivalue
bem - 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*l
funçõ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.fonte
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 paraclearerr()
é a única maneira de redefinir esse indicador); A esse respeito, a resposta de William Pursell é muito melhor.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:
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:
Nesse caso,
feof()
é chamado antes que qualquer dado seja lido, portanto, retorna false. O loop é inserido,fgetc()
é chamado (e retornaEOF
) e a contagem é incrementada. Entãofeof()
é 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 defeof()
NÃO é verificar se a próxima leitura chegará ao final do arquivo. O objetivo defeof()
é distinguir entre um erro de leitura e ter atingido o final do arquivo. Sefread()
retornar 0, você deve usarfeof
/ferror
para decidir se um erro foi encontrado ou se todos os dados foram consumidos. Da mesma forma, sefgetc
retornaEOF
.feof()
só é útil depois que o fread retornou zero oufgetc
retornouEOF
. Antes que isso aconteça,feof()
sempre retornará 0.É sempre necessário verificar o valor de retorno de uma leitura (uma
fread()
, ou umafscanf()
, ou umafgetc()
) antes de chamarfeof()
.Pior ainda, considere o caso em que ocorre um erro de leitura. Nesse caso,
fgetc()
retornaEOF
,feof()
retorna falso e o loop nunca termina. Em todos os casos em quewhile(!feof(p))
é usado, deve haver pelo menos uma verificação dentro do loop paraferror()
, ou pelo menos a condição while deve ser substituída porwhile(!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).fonte
feof(file) || ferror(file)
, portanto, é muito diferente. Mas esta questão não se destina a ser aplicável ao C ++.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.fonte
f = fopen("A:\\bigfile"); while (!feof(f)) { /* remove diskette */ }
ou (indo para testar isso) #f = fopen(NETWORK_FILE); while (!feof(f)) { /* unplug network cable */ }
while(!eof(f))
feof
nã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.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:
fonte
else
não é possível comsizeof(line) >= 2
efgets(line, sizeof(line), file)
mas possível com patológicosize <= 0
efgets(line, size, file)
. Talvez até possível comsizeof(line) == 1
.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 loopfeof(f)
.feof()
não é muito intuitivo. Na minha humilde opinião, o estadoFILE
de final de arquivo deve ser definido comotrue
se 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 usandofgetc()
:Seria ótimo se algo assim funcionasse:
fonte
printf("%c", fgetc(in));
? Esse é um comportamento indefinido.fgetc()
retornaint
, nãochar
.while( (c = getchar()) != EOF)
é muito "algo assim".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.char c
paraint c
obras! Obrigado!!