Защо iostream::eof вътре в условие за цикъл (т.е. `while (!stream.eof())`) се счита за грешно?

Току-що намерих коментар в този отговор, който казва, че използването на iostream::eof в състояние на цикъл е "почти сигурно грешно". Обикновено използвам нещо като while(cin>>n) - което предполагам имплицитно проверява за EOF.

Защо проверката за eof изрично използва while (!cin.eof()) е грешна?

Как се различава от използването на scanf("...",...)!=EOF в C (което често използвам без проблеми)?


person MAK    schedule 09.04.2011    source източник
comment
scanf(...) != EOF също няма да работи в C, защото scanf връща броя на успешно анализираните и присвоени полета. Правилното условие е scanf(...) < n, където n е броят на полетата във форматния низ.   -  person Ben Voigt    schedule 05.04.2012
comment
@Ben Voigt, той ще върне отрицателно число (което EOF обикновено се дефинира като такова), в случай че бъде достигнато EOF   -  person Sebastian    schedule 24.11.2012
comment
@SebastianGodelet: Всъщност ще върне EOF, ако бъде открит край на файла преди първото преобразуване на поле (успешно или не). Ако между полетата се достигне край на файла, ще се върне броя на полетата, които са успешно конвертирани и съхранени. Което прави сравнението с EOF грешно.   -  person Ben Voigt    schedule 24.11.2012
comment
@SebastianGodelet: Не, всъщност не. Той греши, когато казва, че след цикъла няма (лесен) начин да се разграничи правилен вход от неправилен. Всъщност е толкова лесно, колкото да проверите .eof() след излизане от цикъла.   -  person Ben Voigt    schedule 24.11.2012
comment
@Ben Да, за този случай (четене на прост int). Но може лесно да се измисли сценарий, при който while(fail) цикълът завършва както с действителен отказ, така и с eof. Помислете дали имате нужда от 3 int на итерация (да речем, че четете x-y-z точка или нещо подобно), но погрешно има само две int в потока.   -  person sly    schedule 24.11.2012
comment
Този проблем е аналогичен и има същия отговор като въпроса C: Защо while(!feof(file)) винаги е грешен ?. Тъй като флагът се задава само след натискане на EOF.   -  person legends2k    schedule 21.03.2014
comment
Ето отговор на ЧЗВ за C++ по същия въпрос.   -  person legends2k    schedule 21.03.2014
comment
@sly: този сценарий с 3 ints не се обработва правилно от while (in >> x) { if (in >> y >> z) use(x, y, z); else FATAL("got an int not followed by 2 more!"); } if (!eof()) FATAL("didn't get integer where expected");? Ако не, за какво поточно съдържание това не ще работи добре?   -  person Tony Delroy    schedule 07.11.2014
comment
Този въпрос беше публикуван без посочване на авторство в Quora: quora.com/unanswered/ (заглавието на въпроса на Quora съвпада точно със заглавието, което този въпрос имаше, преди да бъде редактиран вчера).   -  person Keith Thompson    schedule 06.05.2019


Отговори (5)


Тъй като iostream::eof ще върне true само след като прочете края на потока. Това не показва, че следващото четене ще бъде краят на потока.

Помислете за това (и приемете, че следващото четене ще бъде в края на потока):

while(!inStream.eof()){
  int data;
  // yay, not end of stream yet, now read ...
  inStream >> data;
  // oh crap, now we read the end and *only* now the eof bit will be set (as well as the fail bit)
  // do stuff with (now uninitialized) data
}

Срещу това:

int data;
while(inStream >> data){
  // when we land here, we can be sure that the read was successful.
  // if it wasn't, the returned stream from operator>> would be converted to false
  // and the loop wouldn't even be entered
  // do stuff with correctly initialized data (hopefully)
}

И по втория ви въпрос: Защото

if(scanf("...",...)!=EOF)

е същото като

if(!(inStream >> data).eof())

инесъщите като

if(!inStream.eof())
    inFile >> data
person Xeo    schedule 09.04.2011
comment
Струва си да се спомене, че if (!(inStream ›› data).eof()) също не прави нищо полезно. Грешка 1: Няма да влезе в условието, ако няма интервал след последната част от данните (последната дата няма да бъде обработена). Грешка 2: Ще влезе в условието, дори ако четенето на данни е неуспешно, стига EOF да не е достигнат (безкраен цикъл, обработка на едни и същи стари данни отново и отново). - person Tronic; 20.01.2013
comment
Малко извън темата, но нека го кажа. Ако някой използва мързелива оценка, ще успее ли този подход без проблем? - person Dilawar; 25.02.2013
comment
Мисля, че си струва да се отбележи, че този отговор е малко подвеждащ. Когато извличате ints или std::strings или подобни, битът EOF е зададен, когато извличате този точно преди края и извличането достигне края. Не е нужно да четете отново. Причината да не се задава при четене от файлове е, че има допълнително \n в края. Разгледах това в друг отговор. Четенето на chars е различен въпрос, защото извлича само един по един и не продължава да удря края. - person Joseph Mansfield; 06.04.2013
comment
Основният проблем е, че само защото не сме достигнали EOF, не означава, че следващото четене ще успее. - person Joseph Mansfield; 06.04.2013
comment
@sftrabbit: всичко е вярно, но не е много полезно... дори и да няма завършващо '\n', разумно е да искате другото завършващо празно пространство да се обработва последователно с другото празно пространство в целия файл (т.е. да се пропусне). Освен това, фина последица от това, когато извлечете този точно преди, е, че while (!eof()) няма да работи върху ints или std::strings, когато входът е напълно празен, така че дори да знаете, че няма завършващ \n е необходима грижа. - person Tony Delroy; 23.04.2013
comment
@TonyD Напълно съгласен. Причината да го казвам е, защото мисля, че повечето хора, когато четат този и подобни отговори, ще си помислят, че ако потокът съдържа "Hello" (без бяло пространство в края или \n) и std::string е извлечено, той ще извлече буквите от H до o , спрете извличането и след това не задайте EOF бита. Всъщност това ще зададе EOF бита, защото EOF е този, който спира извличането. Просто се надявам да изясня това на хората. - person Joseph Mansfield; 23.04.2013
comment
Този отговор трябва да бъде wikid, не минава ден, в който някой да не публикува този антимодел. - person user657267; 27.07.2014
comment
Също така си струва да се спомене. Ако има невалидни данни на входа, четенето може да доведе до преминаване на потока в лошо състояние. Това ще предотврати по-нататъшно четене (освен ако не е изрично изчистено). След това тестът while(!inStream.eof()) ще премине в безкраен цикъл (тъй като не се четат данни и никога не достига eof). - person Martin York; 13.05.2015
comment
// do stuff with (now uninitialized) data Това вече не е вярно от C++11, вижте stackoverflow.com/a/13379073/3002139 - person Baum mit Augen; 21.08.2016
comment
@BaummitAugen Все още остава неинициализирано, ако в потока няма повече знаци например (вижте коментара ми за свързания отговор). Настройката на нула се случва само когато символите са успешно извлечени и след това не могат да бъдат анализирани според очаквания формат - person M.M; 19.11.2019
comment
Може би правех нещо нередно, но на MSVC, ако направя while (std::cin >> word), програмата ми се забива в безкраен цикъл. Но вероятно е по-добре просто да използвам std::getline(). преобразувайте входа в поток от низове, след което направете while (myStringStream >> word), което не се забива в безкраен цикъл. - person apokaliptis; 18.12.2019
comment
тъй като C++11 един от вашите коментари е грешен/подвеждащ: // do stuff with (now uninitialized) data. Тъй като C++11, ако извличането е неуспешно, тогава 0 ще бъде записано на data. Това не променя смисъла на въпроса, но читателите може да се объркат, когато получат 0 и очакват боклук (не казвам, че това е смислено очакване, но такова объркване се случва) - person 463035818_is_not_a_number; 17.11.2020

Най-долу в горния ред: При правилно боравене с бяло пространство, следното е как eof може да се използва (и дори да бъде по-надежден от fail() за проверка на грешки):

while( !(in>>std::ws).eof() ) {  
   int data;
   in >> data;
   if ( in.fail() ) /* handle with break or throw */; 
   // now use data
}    

(Благодаря на Tony D за предложението да подчертаем отговора. Вижте неговия коментар по-долу за пример защо това е по-стабилно.)


Основният аргумент срещу използването на eof() изглежда липсва важна тънкост за ролята на бялото пространство. Моето предложение е, че изричното отчитане на eof() не само не е „винаги грешно“ -- което изглежда е преобладаващо мнение в тази и подобни нишки на SO --, но с правилно боравене с бялото пространство, то осигурява по-чисто и по-надеждно обработване на грешки и е винаги правилното решение (въпреки че не непременно най-трясък).

За да обобщим това, което се предлага като "правилен" ред за прекратяване и четене, е следното:

int data;
while(in >> data) {  /* ... */ }

// which is equivalent to 
while( !(in >> data).fail() )  {  /* ... */ }

Неуспехът поради опит за четене след eof се приема като условие за прекратяване. Това означава, че няма лесен начин да се направи разлика между успешен поток и такъв, който наистина се проваля по причини, различни от eof. Вземете следните потоци:

  • 1 2 3 4 5<eof>
  • 1 2 a 3 4 5<eof>
  • a<eof>

while(in>>data) завършва с набор failbit за всички три входа. В първия и третия също е зададено eofbit. Така че след цикъла се нуждаете от много грозна допълнителна логика, за да разграничите правилния вход (1-ви) от неправилните (2-ри и 3-ти).

Като има предвид, че вземете следното:

while( !in.eof() ) 
{  
   int data;
   in >> data;
   if ( in.fail() ) /* handle with break or throw */; 
   // now use data
}    

Тук in.fail() проверява, че стига да има нещо за четене, то е правилното. Целта му не е просто терминатор на цикъла while.

Дотук добре, но какво се случва, ако има завършващо пространство в потока -- какво звучи като основното безпокойство срещу eof() като терминатор?

Не е необходимо да се отказваме от нашата обработка на грешки; просто изяж бялото пространство:

while( !in.eof() ) 
{  
   int data;
   in >> data >> ws; // eat whitespace with std::ws
   if ( in.fail() ) /* handle with break or throw */; 
   // now use data
}

std::ws пропуска всяко потенциално (нула или повече) завършващо пространство в потока, докато задава eofbit, а не failbit. И така, in.fail() работи според очакванията, стига да има поне една информация за четене. Ако изцяло празните потоци също са приемливи, тогава правилната форма е:

while( !(in>>ws).eof() ) 
{  
   int data;
   in >> data; 
   if ( in.fail() ) /* handle with break or throw */; 
   /* this will never fire if the eof is reached cleanly */
   // now use data
}

Обобщение: Правилно конструираният while(!eof) е не само възможен и не е грешен, но позволява данните да бъдат локализирани в рамките на обхвата и осигурява по-ясно отделяне на проверката за грешки от обичайния бизнес. Като се има предвид това, while(!fail) безспорно е по-разпространен и кратък идиом и може да бъде предпочитан в прости (едни данни за тип четене) сценарии.

person sly    schedule 23.11.2012
comment
Така че след цикъла няма (лесен) начин да се разграничи правилен вход от неправилен. Освен че в единия случай са зададени и eofbit и failbit, в другия е зададено само failbit. Трябва да го тествате само веднъж след прекратяване на цикъла, а не на всяка итерация; той ще напусне цикъла само веднъж, така че трябва да проверите защо е напуснал цикъла само веднъж. while (in >> data) работи добре за всички празни потоци. - person Jonathan Wakely; 25.02.2013
comment
Това, което казвате (и казано по-рано), е, че лошо форматиран поток може да бъде идентифициран като !eof & fail минал цикъл. Има случаи, в които не може да се разчита на това. Вижте коментара по-горе (goo.gl/9mXYX). Така или иначе, аз не предлагам eof-проверете като винаги-по-добрата алтернатива. Просто казвам, че е възможен и (в някои случаи по-подходящ) начин да направите това, а не със сигурност грешен! както има тенденция да се твърди тук в SO. - person sly; 25.02.2013
comment
Като пример, помислете как бихте проверили за грешки, когато данните са структура с претоварен оператор›› четене на множество полета наведнъж - много по-прост случай в подкрепа на вашата точка е stream >> my_int, където потокът съдържа напр. -: eofbit и failbit са зададени. Това е по-лошо от сценария operator>>, където предоставеното от потребителя претоварване има поне опцията да изчисти eofbit, преди да се върне, за да подпомогне използването на while (s >> x). По-общо, този отговор може да използва почистване - само крайният while( !(in>>ws).eof() ) като цяло е стабилен и е заровен в края. - person Tony Delroy; 25.02.2015

Защото ако програмистите не пишат while(stream >> n), те вероятно пишат това:

while(!stream.eof())
{
    stream >> n;
    //some work on n;
}

Тук проблемът е, че не можете да направите some work on n без първо да проверите дали четенето на потока е било успешно, защото ако е било неуспешно, вашето some work on n ще доведе до нежелан резултат.

Целият въпрос е, че eofbit, badbit или failbit се задават след като е направен опит за четене от потока. Така че, ако stream >> n се провали, тогава eofbit, badbit или failbit се задават незабавно, така че по-идиоматично, ако напишете while (stream >> n), тъй като върнатият обект stream се преобразува в false, ако има някаква грешка при четене от потока и следователно цикълът спира. И се преобразува в true, ако четенето е било успешно и цикълът продължава.

person Nawaz    schedule 09.04.2011
comment
Освен споменатия нежелан резултат с извършване на работа върху недефинираната стойност на n, програмата може също така да попадне в безкраен цикъл, ако неуспешната операция на потока не консумира никакви входни данни. - person mastov; 27.04.2018

Другите отговори обясниха защо логиката е грешна в while (!stream.eof()) и как да я поправите. Искам да обърна внимание на нещо различно:

защо проверката за eof изрично използва iostream::eof е грешна?

Най-общо казано, проверката за eof само е погрешна, тъй като извличането на поток (>>) може да се провали, без да достигне края на файла. Ако имате напр. int n; cin >> n; и потокът съдържа hello, тогава h не е валидна цифра, така че извличането ще се провали, без да се стигне до края на входа.

Този проблем, комбиниран с общата логическа грешка при проверка на състоянието на потока преди да се опита да чете от него, което означава, че за N входни елемента цикълът ще се изпълнява N+1 пъти, води до следните симптоми:

  • Ако потокът е празен, цикълът ще се изпълни веднъж. >> ще се провали (няма вход за четене) и всички променливи, които е трябвало да бъдат зададени (от stream >> x), всъщност са неинициализирани. Това води до обработване на ненужни данни, което може да се прояви като безсмислени резултати (често огромни числа).

    (Ако вашата стандартна библиотека отговаря на C++11, сега нещата са малко по-различни: неуспешно >> сега задава числови променливи на 0 вместо да ги остави неинициализирани (с изключение на chars).)

  • Ако потокът не е празен, цикълът ще стартира отново след последното валидно въвеждане. Тъй като в последната итерация всички >> операции са неуспешни, променливите вероятно ще запазят стойността си от предишната итерация. Това може да се прояви като „последният ред се отпечатва два пъти“ или „последният въведен запис се обработва два пъти“.

    (Това трябва да се прояви малко по-различно от C++11 (вижте по-горе): Сега получавате "фантомен запис" от нули вместо повторен последен ред.)

  • Ако потокът съдържа неправилно формирани данни, но вие проверявате само за .eof, ще получите безкраен цикъл. >> няма да успее да извлече никакви данни от потока, така че цикълът се върти на място, без изобщо да стигне до края.


За да обобщим: Решението е да тествате успеха на самата операция >>, а не да използвате отделен метод .eof(): while (stream >> n >> m) { ... }, точно както в C тествате успеха на самото извикване scanf: while (scanf("%d%d", &n, &m) == 2) { ... }.

person melpomene    schedule 04.05.2019
comment
това е най-точният отговор, въпреки че от c++11 не вярвам, че променливите вече са неинициализирани (първият куршум pt) - person csguy; 20.08.2019

Iostream::eof в цикъл се счита за грешен, защото не сме достигнали EOF. Така че това не означава, че следващото четене ще успее.

Ще обясня твърдението си с два примерни кода, които определено ще ви помогнат да разберете концепцията по-добре. Да кажем, когато искаме да прочетем файл с помощта на файлови потоци в C++. И когато използваме цикъл за запис във файл, ако проверим края на файла с помощта на stream.eof(), ние всъщност проверяваме дали файлът е достигнал края или не.

Примерен код

#include<iostream>
#include<fstream>
using namespace std;
int main() {
   ifstream myFile("myfile.txt");
   string x;
   while(!myFile.eof()) {
      myFile >> x;
     // Need to check again if x is valid or eof
     if(x) {
        // Do something with x
     }
   }
}

Когато използваме потока директно в цикъл, няма да проверяваме отново условието.

Примерен код

#include<iostream>
#include<fstream>
using namespace std;
int main() {
   ifstream myFile("myfile.txt");
   string x;
   while(myFile >> x) {
      // Do something with x
      // No checks needed!
   }
}
person Numan Gillani    schedule 25.04.2021