c - 왜 "while(! feof(file))"이 항상 잘못 되었습니까?




while-loop (4)

최근에 많은 게시물에서 이와 같은 파일을 읽으려는 사람들을 최근에 보았습니다.

암호

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

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;
    }
}

while( !feof(fp)) 루프가 잘못되었습니다.


feof ()는 파일의 끝 부분을 읽으 려 시도했는지 여부를 나타냅니다. 즉, 예측 효과가 거의 없음을 의미합니다. 실제로 입력하면 다음 입력 작업이 실패하고 (이전 입력이 BTW에 실패했는지 확실하지 않음), false 일 경우 다음 입력을 확신 할 수 없습니다 작업이 성공합니다. 더 이상, 입력 작업은 파일 끝 (형식화 된 입력에 대한 형식 오류, 순수 IO 실패 - 디스크 오류, 모든 입력 종류에 대한 네트워크 시간 초과)과 같은 다른 이유로 실패 할 수 있습니다. 파일의 끝 (예측할 수있는 Ada 1을 구현하려고 시도한 사람은 공간을 건너 뛰고 대화 형 장치에 바람직하지 않은 영향을 미치기 때문에 복잡 할 수 있다고 말하고 때때로 다음 파일의 입력을 강요합니다. 이전의 핸들을 시작하기 전에 행을 찾으십시오), 실패를 처리 할 수 ​​있어야합니다.

그래서 C 언어의 올바른 관용구는 IO 작업 성공을 루프 조건으로 루프 한 다음 실패의 원인을 테스트하는 것입니다. 예를 들면 :

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

나는 추상적이고 높은 수준의 관점을 제공하고자합니다.

동시성과 동시성

I / O 작업은 환경과 상호 작용합니다. 환경은 프로그램의 일부가 아니며 사용자의 통제하에 있지 않습니다. 환경은 프로그램과 동시에 "동시에"존재합니다. 동시 모든 것과 마찬가지로, "현재 상태"에 대한 질문은 합리적이지 않습니다. 즉, 동시 발생 이벤트에 "동시성"이라는 개념이 없습니다. 국가의 많은 속성은 단순히 동시에 존재 하지 않습니다.

좀 더 정확하게 설명하겠습니다. "더 많은 데이터가 있습니까?"라고 묻고 싶다고 가정 해 보겠습니다. 동시 컨테이너 또는 입출력 시스템에 대해이 질문을 할 수 있습니다. 그러나 대답은 일반적으로 의미가 없기 때문에 의미가 없습니다. 컨테이너가 "예"라고 대답하면 읽을 때까지 더 이상 데이터가 없을 수 있습니다. 마찬가지로, 대답이 "아니오"이면 읽으려고 할 때까지 데이터가 도착했을 수 있습니다. 결론은 가능한 모든 대답에 대한 응답으로 의미있게 행동 할 수 없기 때문에 "나는 데이터가 있습니다"와 같은 속성 없다는 것입니다. (버퍼링 된 입력으로 상황이 약간 나아 졌는데, 어떤 종류의 보증을 구성하는 "예, 데이터가 있습니다"라고 생각할 수도 있지만, 반대의 경우도 처리 할 수 ​​있어야합니다. 출력을 통해 상황 확실히 내가 설명한 것만 큼 나쁘다. 디스크 나 네트워크 버퍼가 꽉 찼는 지 알 수 없다.)

따라서 우리는 I / O 시스템에 I / O 작업을 수행 할 수 있는지 여부를 묻는 것이 불가능하고 실제로는 합리적 이지 않다고 결론을 짓습니다. 우리가 (동시 컨테이너와 마찬가지로) 그것과 상호 작용할 수있는 유일한 방법은 연산을 시도 하고 성공 또는 실패 여부를 확인하는 것입니다. 환경과 상호 작용하는 그 순간, 상호 작용이 실제로 가능한지 여부를 알 수 있으며 그 시점에서 상호 작용을 수행해야합니다. (이 경우 "동기화 지점"입니다.)

EOF

이제 우리는 EOF를 얻습니다. EOF는 시도한 I / O 작업에서 얻은 응답 입니다. 즉, 무언가를 읽거나 쓰려고했지만 읽었을 때 데이터를 읽거나 쓰지 못했을 때 대신 입력 또는 출력의 끝이 발생했습니다. 이것은 C 표준 라이브러리, C ++ iostream 또는 다른 라이브러리이든간에 모든 I / O API에 필수적입니다. I / O 작업이 성공하는 한 향후 작업이 성공할 지 여부를 알 수 없습니다 . 항상 먼저 작업을 시도한 다음 성공 또는 실패에 응답 해야합니다 .

예제들

각 예제에서 I / O 연산을 먼저 시도한 다음 유효하면 결과 소비한다는 점에주의하십시오. 결과는 각 예제에서 다른 모양과 형태를 취할 수 있지만 항상 I / O 작업의 결과를 사용해야한다는 점에 유의하십시오.

  • C stdio, 파일에서 읽기 :

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

    우리가 사용해야하는 결과는 n , 읽기 된 요소의 수 (0 일 수도 있음)입니다.

  • C stdio, scanf :

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

    우리가 사용해야하는 결과는 변환 된 요소의 수인 scanf 의 반환 값입니다.

  • C ++, iostreams 형식화 된 추출 :

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

    우리가 사용해야하는 결과는 부울 컨텍스트에서 평가할 수있는 std::cin 자체이며 스트림이 여전히 good() 상태인지 여부를 알려줍니다.

  • C ++, iostreams getline :

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

    우리가 사용해야하는 결과는 전에와 마찬가지로 std::cin 입니다.

  • POSIX, 버퍼를 플러시하기 위해 write(2) :

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

    여기서 사용하는 결과는 k , 기록 된 바이트 수입니다. 요점은 쓰기 작업 후에 얼마나 많은 바이트가 기록되었는지 알 수 있다는 것입니다.

  • 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);
    

    우리가 사용해야하는 결과는 nbytes , 개행까지의 바이트 수 (파일이 개행 문자로 끝나지 않은 경우 EOF)입니다.

    이 함수는 오류가 발생하거나 EOF에 도달하면 -1 (EOF가 아님)을 명시 적으로 반환합니다.

우리는 실제 단어 "EOF"를 거의 사용하지 않는다는 것을 알 수 있습니다. 우리는 대개 우리에게 더 흥미로운 다른 방법으로 오류 조건을 감지합니다 (예 : 원하는만큼 I / O를 수행하지 못하는 경우). 모든 예에서 EOF 상태가 발생했다는 것을 명시 적으로 알려주는 API 기능이 있지만 이는 실제로 유용한 정보가 아닙니다. 우리가 종종 염려하는 것보다 훨씬 더 자세합니다. 중요한 것은 I / O가 성공했는지 여부입니다.

  • 실제로 EOF 상태를 쿼리하는 마지막 예제입니다. 문자열이 있고 그 문자열이 공백을 제외한 끝에 여분의 비트가없는 정수를 전체적으로 나타내는 지 테스트하려고한다고 가정합니다. C ++ iostream을 사용하면 다음과 같이 진행됩니다.

    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
    }
    

    여기에 두 가지 결과가 사용됩니다. 첫 번째는 스트림 객체 자체인데, 형식화 된 추출이 성공 iss 확인합니다. 그러나 공백을 소비 한 후에도 다른 I / O / 연산 인 iss.get() 을 수행하고 EOF로 실패 할 것으로 예상합니다.이 경우 전체 문자열이 이미 형식화 된 추출에 의해 소비 된 경우입니다.

    C 표준 라이브러리에서는 끝 포인터가 입력 문자열의 끝에 도달했는지 확인하여 strto*l 함수와 비슷한 것을 얻을 수 있습니다.

대답

while(!eof) 는 부적절한 것을 테스트하고 알아야 할 것을 테스트하지 않기 때문에 잘못되었습니다. 결과적으로 실제로 읽지 않은 데이터를 액세스한다고 가정하는 코드를 실수로 실행하는 경우가 발생합니다.


위대한 대답, 나는 그와 같은 반복을하려고했기 때문에 똑같은 것으로 나타났습니다. 이 시나리오에서는 잘못되었지만 EOF에서 정상적으로 끝나는 루프를 원한다면이 방법을 사용하는 것이 좋습니다.

#include <stdio.h>
#include <sys/stat.h>
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
}

읽기 오류가 없으면 작성자가 예상하는 것보다 한 번 더 루프에 들어가기 때문에 잘못되었습니다. 읽기 오류가 있으면 루프가 종료되지 않습니다.

다음 코드를 살펴보십시오.

/* 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! */
        (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;
}

이 프로그램은 입력 스트림의 문자 수보다 큰 문자를 일관되게 인쇄합니다 (읽기 오류가 없다고 가정). 입력 스트림이 비어있는 경우를 고려하십시오.

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

이 경우 feof() 는 데이터를 읽기 전에 호출되므로 false를 반환합니다. 루프가 시작되고 fgetc() 가 호출되고 ( EOF 반환) count가 증가합니다. 그런 다음 feof() 가 호출되고 true를 반환하면 루프가 중단됩니다.

이러한 모든 경우에 발생합니다. feof() 는 스트림에서의 읽기가 파일의 끝에 feof() 할 때까지 true를 반환하지 않습니다. feof() 의 목적은 다음 읽기가 파일의 끝까지 도달 하는지를 검사하는 것이 아닙니다. feof() 의 목적은 읽기 오류와 파일 끝에 도달 feof() 를 구별하는 것입니다. fread() 가 0을 반환하면 feof / ferror 를 사용하여 결정해야합니다. 마찬가지로 fgetcEOF 반환하면 마찬가지입니다. feof() 는 fread가 0을 반환하거나 fgetc 가 EOF를 반환 한 후에 만 유용합니다. 그 전에 feof() 는 항상 0을 반환합니다.

feof() 를 호출하기 전에 항상 read ( fread() , fscanf() 또는 fgetc() )의 반환 값을 검사해야합니다.

더 나쁜 것은 읽기 오류가 발생한 경우를 생각해보십시오. 이 경우 fgetc()EOF 반환하고 feof() 는 false를 반환하며 루프는 종료되지 않습니다. while(!feof(p)) 가 사용되는 모든 경우에 ferror() 대한 루프 내부의 검사가 적어도 있어야하며 while 조건은 while(!feof(p) && !ferror(p)) 로 대체되어야합니다 while(!feof(p) && !ferror(p)) 무한 루프가 발생할 가능성이 매우 높습니다. 유효하지 않은 데이터가 처리 될 때 모든 종류의 쓰레기가 튀어 나올 수 있습니다.

요약하자면, " while(!feof(f)) "을 쓰는 것이 의미 상 정확할 수있는 상황이 결코 없다는 것을 확실하게 말할 수는 없지만 while(!feof(f)) 루프 내에 또 다른 체크가 있어야 하지만 읽기 오류에서 무한 루프를 피하십시오.) 그것은 거의 틀림없이 항상 틀린 경우입니다. 심지어 올바른 경우가 생겨도 코드를 작성하는 것이 옳지 않다는 것은 매우 관용적으로 잘못되었습니다. 그 코드를 보는 사람은 즉시 "버그"라고 주저하고 말합니다. 그리고 저자를 두들겨 패할 수도 있습니다 (저자가 귀하의 상사 인 경우를 제외하고는 재량에 대해 자문을 구하십시오).





feof