“while (! feof (file))”이 왜 항상 잘못된 것입니까?


573

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

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

이 루프에 어떤 문제가 있습니까?



답변:


453

추상적이고 높은 수준의 관점을 제공하고 싶습니다.

동시성과 동시성

I / O 작업은 환경과 상호 작용합니다. 환경은 프로그램의 일부가 아니며 사용자가 통제 할 수 없습니다. 환경은 프로그램과 "동시에"존재합니다. 모든 것이 동시에 그렇듯이 "현재 상태"에 대한 질문은 의미가 없습니다. 동시 이벤트에 대해 "동시"라는 개념은 없습니다. 상태의 많은 속성은 단순히 동시에 존재 하지 않습니다 .

좀 더 정확하게 말씀 드리겠습니다. "더 많은 데이터가 있습니까?" 이를 동시 컨테이너 또는 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 ++, iostream 형식의 추출 :

    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).

    -1오류가 발생하거나 EOF에 도달하면 함수는 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스트림 객체 자체이며 형식화 된 추출이 value성공 했는지 확인합니다 . 그러나 공백을 사용한 후에도 다른 I / O / 연산을 수행 iss.get()하고 EOF로 실패 할 것으로 예상됩니다. 이는 전체 문자열이 이미 형식화 된 추출에 의해 소비 된 경우입니다.

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

대답

while(!feof)관련이없는 것을 테스트하고 알아야 할 것을 테스트하지 않기 때문에 잘못되었습니다. 결과적으로 실제로 이런 일이 발생하지 않았을 때 성공적으로 읽은 데이터에 액세스한다고 가정하는 코드를 잘못 실행하고 있습니다.


34
@CiaPan : 나는 그것이 사실이라고 생각하지 않습니다. C99와 C11 모두 가능합니다.
Kerrek SB

11
그러나 ANSI C는 그렇지 않습니다.
CiaPan

3
@JonathanMee : 내가 언급 한 모든 이유는 나쁘다 : 당신은 미래를 볼 수 없다. 앞으로 어떤 일이 일어날 지 알 수 없습니다.
Kerrek SB

3
@JonathanMee : 예, 일반적 으로이 검사를 작업에 결합 할 수 있지만 (대부분의 iostream 작업은 자체적으로 부울 변환이있는 스트림 객체를 반환하므로) 그렇지 않다는 것을 분명히합니다. 반환 값을 무시합니다.
Kerrek SB

4
세 번째 문단은 인정되고 찬성 된 답변에 대해 오해의 소지가 있거나 부정확합니다. feof()"더 많은 데이터가 있는지 I / O 시스템에 묻지 않습니다". feof()(Linux) 맨 페이지 에 따르면 : "스트림이 가리키는 스트림의 파일 끝 표시기를 테스트하여 설정되어 있으면 0이 아닌 값을 반환합니다." (또한 명시적인 호출 clearerr()은이 표시기를 재설정하는 유일한 방법입니다); 이와 관련하여 William Pursell의 답변이 훨씬 낫습니다.
Arne Vogel

234

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

다음 코드를 고려하십시오.

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

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

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

이 경우 feof()데이터를 읽기 전에 호출되므로 false를 반환합니다. 루프가 입력되고을 fgetc()호출 (및 반환 EOF)하며 카운트가 증가합니다. 그런 다음 feof()호출되어 true를 반환하여 루프가 중단됩니다.

이것은 모든 경우에 발생합니다. feof()때까지 true를 반환하지 않습니다 스트림의 읽기는 파일의 끝을 발견. 목적은 feof()다음 읽기가 파일의 끝에 도달하는지 확인하지 않는 것입니다. feof()읽기 오류와 파일 끝에 도달 한 것을 구별 하는 것이 목적입니다 . 경우 fread()0을 반환, 당신은 사용해야합니다 feof/ ferror오류가 발생했습니다 또는 모든 데이터가 소모 된 경우 여부를 결정합니다. 마찬가지로 if를 fgetc반환합니다 EOF. fread가 0 을 반환 하거나 반환 한 후에feof() 만 유용합니다 . 그 전에 항상 0을 반환합니다.fgetcEOFfeof()

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

더 나쁜 것은 읽기 오류가 발생하는 경우를 고려하십시오. 이 경우, fgetc()반환 EOF, feof()false를 반환하고, 루프는 종료하지 않습니다. while(!feof(p))사용되는 모든 경우 에 대해 적어도 루프 내부에 대한 점검이 ferror()있거나 최소한 while 조건이 대체되어야 while(!feof(p) && !ferror(p))하거나 무한 루프의 실제 가능성이있을 수 있습니다. 유효하지 않은 데이터가 처리되고 있습니다.

따라서 요약하자면, " while(!feof(f))"라고 쓰는 것이 의미 론적으로 올바른 상황은 없다고 확신 할 수는 없지만 ( 읽기 오류에서 무한 루프를 피하기 위해 중단으로 루프 내부에 다른 검사 가 있어야 하지만) ), 거의 항상 틀린 경우입니다. 그리고 올바른 경우가 발생하더라도 관용적으로 잘못되어 코드를 작성하는 올바른 방법이 아닐 수 있습니다. 이 코드를 보는 사람은 즉시 주저하고 "버그입니다"라고 말합니다. 저자가 당신의 상사 인 경우를 제외하고 저자를 때릴 수 있습니다.


7
물론 그것은 틀리다. 그러나 그 점을 제외하고는 "얼마나 추악한"것은 아니다.
nobar

89
많은 사람들이 빠른 수정을 위해 여기에 올 것이라고 상상할 때 올바른 코드의 예를 추가해야합니다.
jleahy

6
@Thomas : 저는 C ++ 전문가는 아니지만 file.eof ()가와 동일한 결과를 효과적으로 반환한다고 생각 feof(file) || ferror(file)하므로 매우 다릅니다. 그러나이 질문은 C ++에는 적용되지 않습니다.
William Pursell

6
@ m-ric도 옳지 않습니다. 실패한 읽기를 처리하려고 시도하기 때문입니다.
Mark Ransom

4
이것이 실제 정답입니다. feof ()는 이전 읽기 시도의 결과를 아는 데 사용됩니다. 따라서 아마도 루프 브레이크 조건으로 사용하고 싶지 않을 것입니다. +1
Jack

63

아니요 항상 틀린 것은 아닙니다. 루프 조건이 "파일의 과거 끝을 읽으려고 시도하지 않은 동안"인 경우을 사용 while (!feof(f))합니다. 그러나 이것은 일반적인 루프 조건이 아닙니다. 일반적으로 다른 것을 테스트하려고합니다 (예 : "더 읽을 수 있습니까"). while (!feof(f))잘못이 아니고 잘못 사용되었습니다 .


1
궁금합니다 ... f = fopen("A:\\bigfile"); while (!feof(f)) { /* remove diskette */ }또는 (이것을 테스트하려고합니다)f = fopen(NETWORK_FILE); while (!feof(f)) { /* unplug network cable */ }
pmg

1
@pmg : "공통 루프 조건이 아님"이라고 말했듯이. 나는 정말 그것을 필요로 한 모든 경우에 생각할 수 없다, 보통 내가 관심이있는 오류 처리의 의미가 모두와 함께 "나는 내가 원하는 것을 읽을 수 있었다"
에릭

@pmg : 말했듯이, 거의 원하지 않습니다while(!eof(f))
Erik

9
보다 정확하게는 조건은 "파일 끝을 지나서 읽으려고하지 않았지만 읽기 오류가 없었습니다." feof는 파일 끝을 감지하는 것이 아닙니다. 오류로 인해 읽기가 짧은 지 또는 입력이 소진 되었는가를 판단하는 것입니다.
William Pursell

35

feof()파일 끝을 지나서 읽으려고했는지 여부를 나타냅니다. 즉, 예측 효과가 거의 없음을 의미합니다. 사실 경우 다음 입력 작업이 실패 할 것이라고 확신하지만 (이전 작업이 BTW에 실패했는지 확실하지 않은 경우) 거짓 인 경우 다음 입력이 확실하지 않습니다 작업이 성공합니다. 또한 파일 끝 이외의 다른 이유로 입력 작업이 실패 할 수 있습니다 (형식화 된 입력의 경우 형식 오류, 순수한 IO 오류 (디스크 오류, 네트워크 시간 초과-모든 입력 종류의 경우)). 파일의 끝 (그리고 예측 가능한 Ada one을 구현하려고 시도한 사람은 공백을 건너 뛸 필요가 있으면 복잡 할 수 있으며 대화 형 장치에 바람직하지 않은 영향을 미치며 때로는 다음 입력을 강요한다고 말합니다. 이전의 처리를 시작하기 전에 라인),

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

2
파일의 끝에 도달하는 것은 오류가 아니므로 "파일 끝 이외의 다른 이유로 입력 작업이 실패 할 수 있습니다"라는 문구에 의문을 제기합니다.
William Pursell

@WilliamPursell은 eof에 도달하는 것이 반드시 오류는 아니지만 eof로 인해 입력 작업을 수행 할 수없는 것은 하나입니다. 그리고 C에서 입력 작업을 실패하지 않고 eof를 안정적으로 감지하는 것은 불가능합니다.
AProgrammer

지난 동의 else불가능 sizeof(line) >= 2하고 fgets(line, sizeof(line), file)병적으로 만 가능 size <= 0하고 fgets(line, size, file). 로 가능할 수도 있습니다 sizeof(line) == 1.
chux-복원 Monica Monica

1
"예측 적 가치"라는 말은 ... 그런 식으로 생각하지 않았습니다. 내 세상에서는 feof(f)아무것도 예측하지 않습니다. PREVIOUS 조작이 파일의 끝에 도달했음을 나타냅니다. 더 이상 아무것도 없습니다. 그리고 이전 작업이 없으면 (방금 연 경우) 파일이 비어 있어도 파일 끝을보고하지 않습니다. 따라서 위의 다른 대답에서 동시성 설명 외에도 반복하지 않는 이유가 있다고 생각하지 않습니다 feof(f).
BitTickler

@AProgrammer : 요청 A는 수율이 더 이상 데이터를 사용할 수 없기 때문에 때문에 "고정"EOF의 여부 제로 것을 "N은 바이트까지 읽기" 아직 에러 아니다. feof ()는 향후 요청이 데이터를 생성 할 것으로 확실하게 예측할 수 없지만 향후 요청 이 그렇지 않을 것임을 확실하게 나타낼 수 있습니다 . 아마도 "미래의 읽기 요청이 성공할 가능성이 있음"을 나타내는 상태 함수가 있어야하며, 일반 파일의 끝까지 읽은 후에 품질 구현에서는 미래의 읽기가 어떤 이유로 인해 성공 하지 않을 것이라고 말해야 합니다. 그들이 믿습니다 .
supercat 2019

0

feof()매우 직관적이지 않습니다. 매우 겸손한 견해로는 FILE파일의 끝 상태는 true읽기 작업으로 인해 파일 끝에 도달하면 설정해야합니다 . 대신, 각 읽기 작업 후에 파일 끝에 도달했는지 수동으로 확인해야합니다. 예를 들어, 다음을 사용하여 텍스트 파일에서 읽을 경우 이와 같은 기능이 작동합니다 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;
}

이와 같은 것이 대신 작동하면 좋을 것입니다.

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

1
printf("%c", fgetc(in));? 정의되지 않은 동작입니다. 아닌을 fgetc()반환합니다 . intchar
Andrew Henle

표준 관용구 while( (c = getchar()) != EOF)는 매우 "이것 같은 것"인 것 같습니다.
William Pursell

while( (c = getchar()) != EOF)GNU C 10.1.0을 실행하는 데스크탑 중 하나에서 작동하지만 GNU C 9.3.0을 실행하는 Raspberry Pi 4에서는 작동하지 않습니다. 내 RPi4에서 파일 끝을 감지하지 못하고 계속 진행됩니다.
Scott Deagan

@AndrewHenle 당신이 맞아요! 변경 char cint c작품! 감사!!
Scott Deagan
당사 사이트를 사용함과 동시에 당사의 쿠키 정책개인정보 보호정책을 읽고 이해하였음을 인정하는 것으로 간주합니다.
Licensed under cc by-sa 3.0 with attribution required.