Programing

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

lottogame 2020. 2. 10. 22:00
반응형

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


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

암호

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

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


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

동시성과 동시성

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


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

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

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


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


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

참고 URL : https://stackoverflow.com/questions/5431941/why-is-while-feof-file-always-wrong



반응형