Ostatnio widziałem ludzi próbujących czytać takie pliki w wielu postach:
#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;
}
Co jest nie tak z tą pętlą?
feof()
do sterowania pętląOdpowiedzi:
Chciałbym przedstawić abstrakcyjną perspektywę wysokiego poziomu.
Współbieżność i równoczesność
Operacje we / wy współdziałają ze środowiskiem. Środowisko nie jest częścią twojego programu i nie jest pod twoją kontrolą. Środowisko naprawdę istnieje „równolegle” z twoim programem. Podobnie jak w przypadku wszystkich rzeczy współbieżnych, pytania dotyczące „obecnego stanu” nie mają sensu: nie ma pojęcia „jednoczesności” między równoległymi zdarzeniami. Wiele właściwości stanu po prostu nie istnieje jednocześnie.
Pozwól, że uściślę: Załóżmy, że chcesz zapytać „czy masz więcej danych”. Możesz zapytać o współbieżny kontener lub swój system I / O. Ale odpowiedź jest generalnie bezczynna, a zatem bez znaczenia. Co z tego, jeśli pojemnik powie „tak” - zanim spróbujesz czytać, może już nie mieć danych. Podobnie, jeśli odpowiedź brzmi „nie”, do czasu próby odczytania dane mogły przybyć. Wniosek jest taki, że po prostu jestżadna właściwość, taka jak „Mam dane”, ponieważ nie można podjąć znaczących działań w odpowiedzi na jakąkolwiek możliwą odpowiedź. (Sytuacja jest nieco lepsza w przypadku buforowanych danych wejściowych, w których można uzyskać odpowiedź „tak, mam dane”, która stanowi pewną gwarancję, ale nadal będziesz musiał poradzić sobie z przypadkiem odwrotnym. I przy wyjściu z sytuacji jest z pewnością tak źle, jak opisałem: nigdy nie wiadomo, czy ten dysk lub bufor sieciowy jest pełny.)
Stwierdzamy zatem, że nie jest możliwe, a wręcz nieuzasadnione , zapytanie systemu we / wy, czy będzie on w stanie wykonać operację we / wy. Jedynym możliwym sposobem na interakcję z nim (podobnie jak przy równoczesnym kontenerze) jest próba wykonania operacji i sprawdzenie, czy się powiodła, czy nie. W tym momencie, w którym wchodzisz w interakcję ze środowiskiem, wtedy i tylko wtedy możesz wiedzieć, czy interakcja była rzeczywiście możliwa, i wtedy musisz zobowiązać się do wykonania interakcji. (Jest to „punkt synchronizacji”, jeśli chcesz).
EOF
Teraz dochodzimy do EOF. EOF to odpowiedź uzyskana z próby operacji we / wy. Oznacza to, że próbujesz coś odczytać lub napisać, ale nie udało ci się odczytać ani zapisać żadnych danych, a zamiast tego napotkano koniec wejścia lub wyjścia. Dotyczy to zasadniczo wszystkich interfejsów API we / wy, bez względu na to, czy jest to biblioteka standardowa C, iostreams C ++, czy inne biblioteki. Dopóki operacje we / wy zakończą się powodzeniem , po prostu nie będzie wiadomo, czy dalsze przyszłe operacje zakończą się powodzeniem. Zawsze musisz najpierw wypróbować operację, a następnie zareagować na sukces lub porażkę.
Przykłady
W każdym z przykładów zwróć uwagę, że najpierw próbujemy operacji We / Wy, a następnie wykorzystujemy wynik, jeśli jest prawidłowy. Zauważ ponadto, że zawsze musimy użyć wyniku operacji We / Wy, chociaż wynik ma różne kształty i formy w każdym przykładzie.
C stdio, odczytane z pliku:
Wynik, którego musimy użyć
n
, to liczba odczytanych elementów (która może wynosić zaledwie zero).C stdio
scanf
:Wynik, którego musimy użyć, to zwracana wartość
scanf
liczby przekonwertowanych elementów.C ++, ekstrakcja sformatowana przez iostreams:
Wynik, którego musimy użyć, to
std::cin
sam, który można ocenić w kontekście logicznym i mówi nam, czy strumień jest nadal wgood()
stanie.C ++, iostreams getline:
Rezultat, którego musimy użyć, jest ponownie
std::cin
, tak jak poprzednio.POSIX,
write(2)
aby opróżnić bufor:Wynik, którego tu używamy
k
, to liczba zapisanych bajtów. Chodzi o to, że możemy wiedzieć tylko, ile bajtów zostało napisanych po operacji zapisu.POSIX
getline()
Wynik, którego musimy użyć
nbytes
, to liczba bajtów do nowej linii włącznie (lub EOF, jeśli plik nie kończy się nową linią).Zauważ, że funkcja wyraźnie zwraca
-1
(a nie EOF!), Gdy wystąpi błąd lub osiągnie EOF.Możesz zauważyć, że bardzo rzadko przeliterujemy słowo „EOF”. Zazwyczaj wykrywamy stan błędu w inny sposób, który jest dla nas od razu interesujący (np. Brak wykonania tak dużej liczby operacji we / wy, jak chcieliśmy). W każdym przykładzie jest jakaś funkcja API, która może nam wyraźnie powiedzieć, że napotkano stan EOF, ale w rzeczywistości nie jest to bardzo przydatna informacja. To jest o wiele więcej szczegółów, niż nam się często zależy. Liczy się to, czy We / Wy się powiodło, bardziej niż to, w jaki sposób zawiodło.
Ostatni przykład, który faktycznie pyta o stan EOF: Załóżmy, że masz ciąg znaków i chcesz przetestować, czy reprezentuje on liczbę całkowitą w całości, bez dodatkowych bitów na końcu, z wyjątkiem białych znaków. Przy użyciu iostreams C ++ wygląda to tak:
Używamy tutaj dwóch wyników. Pierwszym z nich jest
iss
sam obiekt strumienia, aby sprawdzić, czy sformatowana ekstrakcjavalue
zakończyła się powodzeniem. Ale potem, po zużyciu spacji, wykonujemy kolejną operację I / O /iss.get()
i oczekujemy, że zakończy się ona niepowodzeniem jako EOF, co ma miejsce, jeśli cały ciąg został już wykorzystany przez sformatowaną ekstrakcję.W standardowej bibliotece C można osiągnąć coś podobnego za pomocą
strto*l
funkcji, sprawdzając, czy wskaźnik końca osiągnął koniec ciągu wejściowego.Odpowiedź
while(!feof)
jest błędny, ponieważ testuje coś, co jest nieistotne i nie sprawdza się pod kątem czegoś, co musisz wiedzieć. Powoduje to, że błędnie wykonujesz kod, który zakłada, że uzyskuje dostęp do danych, które zostały pomyślnie odczytane, podczas gdy w rzeczywistości tak się nigdy nie stało.źródło
feof()
nie „pyta systemu I / O, czy ma więcej danych”.feof()
, zgodnie ze stroną podręcznika (Linux) : „testuje wskaźnik końca pliku dla strumienia wskazywanego przez strumień, zwracając wartość niezerową, jeśli jest ustawiony”. (również wyraźne wezwanie doclearerr()
jest jedynym sposobem na zresetowanie tego wskaźnika); Pod tym względem odpowiedź Williama Pursella jest znacznie lepsza.Jest to błędne, ponieważ (przy braku błędu odczytu) wchodzi w pętlę jeszcze raz, niż oczekuje autor. Jeśli wystąpi błąd odczytu, pętla nigdy się nie kończy.
Rozważ następujący kod:
Ten program będzie konsekwentnie drukował jeden większy niż liczba znaków w strumieniu wejściowym (przy założeniu braku błędów odczytu). Rozważ przypadek, w którym strumień wejściowy jest pusty:
W takim przypadku
feof()
jest wywoływany przed odczytaniem jakichkolwiek danych, więc zwraca wartość false. Pętla zostaje wprowadzona,fgetc()
nazywa się (i zwracaEOF
), a liczba jest zwiększana. Następniefeof()
jest wywoływana i zwraca wartość true, co powoduje przerwanie pętli.Dzieje się tak we wszystkich takich przypadkach.
feof()
nie zwraca prawda aż po odczytu na strumień napotka koniec pliku. Celemfeof()
NIE jest sprawdzenie, czy następny odczyt dotrze do końca pliku. Celemfeof()
jest rozróżnienie między błędem odczytu a osiągnięciem końca pliku. Jeślifread()
zwraca 0, musisz użyćfeof
/,ferror
aby zdecydować, czy wystąpił błąd lub czy wszystkie dane zostały wykorzystane. Podobnie, jeślifgetc
powróciEOF
.feof()
jest użyteczne tylko wtedy, gdy fread zwrócił zero lubfgetc
powróciłEOF
. Zanim to nastąpi,feof()
zawsze zwróci 0.Zawsze konieczne jest sprawdzenie wartości zwracanej odczytu (an
fread()
, anfscanf()
lub anfgetc()
) przed wywołaniemfeof()
.Co gorsza, rozważ przypadek, w którym występuje błąd odczytu. W takim przypadku
fgetc()
zwracaEOF
,feof()
zwraca false, a pętla nigdy się nie kończy. We wszystkich przypadkach, w którychwhile(!feof(p))
jest używany, wewnątrz pętli musi być co najmniej sprawdzenieferror()
, a przynajmniej warunek while powinien zostać zastąpionywhile(!feof(p) && !ferror(p))
lub istnieje bardzo realna możliwość nieskończonej pętli, prawdopodobnie wyrzucającej wszelkiego rodzaju śmieci, ponieważ nieprawidłowe dane są przetwarzane.Podsumowując, chociaż nie mogę z całą pewnością stwierdzić, że nigdy nie ma sytuacji, w której napisanie „
while(!feof(f))
” byłoby poprawne semantycznie (chociaż musi być jeszcze jedno sprawdzenie wewnątrz pętli z przerwą, aby uniknąć nieskończonej pętli przy błędzie odczytu ) jest tak, że prawie na pewno zawsze się myli. I nawet jeśli kiedykolwiek pojawił się przypadek, w którym byłby poprawny, jest tak idiomatycznie zły, że nie byłby to właściwy sposób na napisanie kodu. Każdy, kto zobaczy ten kod, powinien natychmiast zawahać się i powiedzieć „to błąd”. I ewentualnie uderzyć autora (chyba że autor jest twoim szefem, w takim przypadku zaleca się dyskrecję).źródło
feof(file) || ferror(file)
, więc jest bardzo różny. Ale to pytanie nie ma dotyczyć C ++.Nie, nie zawsze jest źle. Jeśli warunek pętli jest „gdy nie próbowaliśmy odczytać końca pliku”, użyj
while (!feof(f))
. Nie jest to jednak częsty warunek pętli - zwykle chcesz przetestować coś innego (na przykład „czy mogę przeczytać więcej”).while (!feof(f))
nie jest źle, jest po prostu źle użyte .źródło
f = fopen("A:\\bigfile"); while (!feof(f)) { /* remove diskette */ }
lub (zamierzam to przetestować)f = fopen(NETWORK_FILE); while (!feof(f)) { /* unplug network cable */ }
while(!eof(f))
feof
nie chodzi o wykrywanie końca pliku; chodzi o określenie, czy odczyt był krótki z powodu błędu lub z powodu wyczerpania danych wejściowych.feof()
wskazuje, czy ktoś próbował odczytać poza końcem pliku. Oznacza to, że ma niewielki efekt predykcyjny: jeśli jest to prawda, masz pewność, że następna operacja wejścia nie powiedzie się (nie jesteś pewien, że poprzednia nie powiodła się BTW), ale jeśli jest to fałsz, nie jesteś pewien, operacja się powiedzie. Ponadto operacje wprowadzania mogą się nie powieść z innych powodów niż koniec pliku (błąd formatu sformatowanego wejścia, błąd samej operacji we / wy - awaria dysku, przekroczenie limitu czasu sieci - dla wszystkich rodzajów danych wejściowych), więc nawet jeśli możesz przewidywać koniec pliku (i każdy, kto próbował zaimplementować Ada one, który jest predykcyjny, powie ci, że może on być skomplikowany, jeśli potrzebujesz pominąć spacje, i że ma to niepożądane skutki na urządzeniach interaktywnych - czasami wymuszając wprowadzanie następnego linia przed rozpoczęciem obsługi poprzedniej),Tak więc poprawnym idiomem w C jest zapętlenie z sukcesem operacji we / wy jako warunek pętli, a następnie przetestowanie przyczyny niepowodzenia. Na przykład:
źródło
else
nie możliwesizeof(line) >= 2
, afgets(line, sizeof(line), file)
jednak możliwe patologicznysize <= 0
ifgets(line, size, file)
. Może nawet możliwe zsizeof(line) == 1
.feof(f)
NIC NIE PRZEWIDZIE. Stwierdza, że POPRZEDNIA operacja dotarła do końca pliku. Nic dodać nic ująć. A jeśli nie było poprzedniej operacji (właśnie ją otworzyłem), nie zgłasza końca pliku, nawet jeśli plik był pusty na początek. Tak więc, oprócz wyjaśnienia dotyczącego współbieżności w innej odpowiedzi powyżej, nie sądzę, aby istniał jakiś powód, aby nie zapętlaćfeof(f)
.feof()
nie jest bardzo intuicyjny. Moim bardzo skromnym zdaniem stanFILE
końca pliku powinien zostać ustawiony na,true
jeśli jakakolwiek operacja odczytu spowoduje osiągnięcie końca pliku. Zamiast tego musisz ręcznie sprawdzić, czy do końca pliku został osiągnięty po każdej operacji odczytu. Na przykład coś takiego będzie działać, jeśli odczytujesz z pliku tekstowego przy użyciufgetc()
:Byłoby wspaniale, gdyby zamiast tego działało coś takiego:
źródło
printf("%c", fgetc(in));
? To nieokreślone zachowanie.fgetc()
zwracaint
niechar
.while( (c = getchar()) != EOF)
to bardzo „coś takiego”.while( (c = getchar()) != EOF)
działa na jednym z moich komputerów z systemem GNU C 10.1.0, ale nie działa na moim Raspberry Pi 4 z systemem GNU C 9.3.0. W moim RPi4 nie wykrywa końca pliku i po prostu działa.char c
doint c
pracy! Dzięki!!