Чому iostream :: eof всередині циклу (тобто `while (! Stream.eof ())`) вважається неправильним?


595

Щойно я знайшов коментар у цій відповіді, в якому говорилося, що використовувати iostream::eofв циклі стан "майже напевно неправильно". Я, як правило, використовую щось на кшталт while(cin>>n)- яке, мабуть, неявно перевіряє на EOF.

Чому перевірка eof явно використовує while (!cin.eof())неправильно?

Чим він відрізняється від використання scanf("...",...)!=EOFв C (яким я часто користуюся без проблем)?


21
scanf(...) != EOFтакож не працюватиме в C, оскільки scanfповертає кількість полів, успішно проаналізованих та призначених. Правильна умова - scanf(...) < nде nкількість полів у рядку формату.
Бен Войгт

5
@Ben Voigt, він поверне від'ємне число (яке EOF зазвичай визначається як таке) у разі досягнення EOF
Себастьян

19
@SebastianGodelet: Насправді він повернеться, EOFякщо кінець файлу виявиться до першого перетворення поля (вдалого чи ні). Якщо кінець файлу буде досягнуто між полями, він поверне кількість полів, успішно перетворених та збережених. Що робить порівняння з EOFнеправильним.
Бен Войгт

1
@SebastianGodelet: Ні, не дуже. Він помиляється, коли каже, що "поза циклом немає (легкого) способу відрізнити правильний вклад від неправильного". Насправді це так просто, як перевірити .eof()після виходу циклу.
Бен Войгт

2
@Ben Так, для цього випадку (читання простого int). Але можна легко придумати сценарій, коли while(fail)цикл закінчується як фактичним збоєм, так і eof. Подумайте, якщо вам потрібні 3 ints за кожну ітерацію (скажімо, ви читаєте точку xyz чи щось таке), але помилково є лише два ints у потоці.
лукавий

Відповіді:


544

Тому що 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

12
Варто згадати, що якщо (! (InStream >> data) .eof ()) теж нічого корисного не зробить. Помилковість 1: Він не буде вводити умову, якщо після останнього фрагмента даних не було пробілів (остання дата не оброблятиметься). Помилковість 2: Він буде вводити умову, навіть якщо зчитування даних не вдалося, доки EOF не було досягнуто (нескінченний цикл, обробка тих же старих даних знову і знову).
Тронік

4
Я думаю, що варто зазначити, що ця відповідь є дещо оманливою. Під час вилучення ints або std::stringS або подібне, то біт EOF буде встановлено , коли ви витягаєте одне право до кінця і видобуток потрапляє в кінець. Вам не потрібно читати знову. Причина, яку він не встановлює при читанні з файлів, полягає в тому, що \nв кінці є додаткові . Я висвітлював це в іншій відповіді . Читання chars - інша справа, тому що вона витягує лише одну за одною і не продовжує досягати кінця.
Джозеф Менсфілд

79
Основна проблема полягає в тому, що те, що ми не дійшли до EOF, не означає, що наступне читання буде успішним .
Джозеф Менсфілд

1
@sftrabbit: все вірно, але не дуже корисно ... навіть якщо немає ніяких трейлінгів \ \ n ', розумно хотіти, щоб інші трейлінг-пробіли оброблялися послідовно з іншими пробілами у всьому файлі (тобто пропускалися). Крім того, витончений наслідок "коли ви вилучаєте правий раніше" - це те, що while (!eof())він не буде "працювати" на ints або std::strings, коли вхід повністю порожній, тому навіть знаючи, що не \nпотрібно ніякого останнього догляду.
Тоні Делрой

2
@TonyD Повністю згоден. Причина , чому я говорю це, тому що я думаю , що більшість людей , коли вони читають це і подібні відповіді будуть думати , що якщо потік не містить "Hello"(не замикається прогалини або \n) і std::stringвитягується, він буде отримувати листи від Hдо o, зупинки вилучення і тоді не встановлюйте біт EOF. Фактично, він встановив би біт EOF, оскільки саме EOF зупинив видобуток. Просто сподіваюся зрозуміти це для людей.
Джозеф Менсфілд

103

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

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

( Дякую Тоні Д за пропозицію підкреслити відповідь. Дивіться його коментар нижче для прикладу, чому це більш надійно. )


Основний аргумент проти використання, 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()підтверджується, що поки є що читати, це правильне. Його мета - не простий термінатор, а цикл.

Поки що добре, але що станеться, якщо в потоці є простір - що звучить як головне занепокоєння 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)мабуть, є більш поширеною та стислою ідіомою, і її можна віддати перевагу у простих (одинарних даних на тип прочитаного).


6
" Отже, минулий цикл не існує (легкого) способу відрізнити правильний вхід від неправильного. " За винятком того, що в одному випадку обидва eofbitі failbitвстановлені, в іншому встановлено лише те, що failbitє. Вам потрібно лише перевірити, що раз після закінчення циклу, не на кожній ітерації; він залишить цикл лише один раз, тому вам потрібно лише перевірити, чому він залишив цикл один раз. while (in >> data)прекрасно працює для всіх порожніх потоків.
Джонатан Уейклі

3
Що ви говорите (і пункт, зроблений раніше), це те, що поганий форматированний потік може бути ідентифікований як !eof & failминулий цикл. Бувають випадки, коли на це не можна покластися. Дивіться коментар вище ( goo.gl/9mXYX ). Так чи інакше, я не пропоную eof-чек як завжди кращу альтернативу. Я просто кажу, що це можливий і (в деяких випадках більш підходящий) спосіб зробити це, а не "напевне, неправильно!" як це, як правило, заявлено тут, в SO.
лукавий

2
"Як приклад, розгляньте, як ви перевіряли б помилки, коли дані є структурою з перевантаженим оператором >> зчитування одночасно декількох полів" - набагато простішим випадком, який підтримує вашу точку, є те, stream >> my_intде потік містить напр. "-": eofbitі failbitє набір. Це гірше, ніж operator>>сценарій, коли передбачені користувачем перевантаження принаймні мають можливість очищення eofbitперед поверненням, щоб допомогти користуватися підтримкою while (s >> x). Більш загально, ця відповідь може використовувати очищення - лише фінал, while( !(in>>ws).eof() )як правило, є надійним, і він захований в кінці.
Тоні Делрой

74

Тому що якщо програмісти не пишуть 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якщо читання було успішним і цикл триває.


1
Крім згаданого «небажаного результату», виконуючи роботу над невизначеним значенням n, програма також може потрапити в нескінченний цикл , якщо невдала операція потоку не потребує жодного вводу.
мастов

10

Інші відповіді пояснили, чому логіка неправильна 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) { ... }.


1
це найточніша відповідь, хоча станом на c ++ 11, я не вірю, що змінні вже не ініціалізуються (перша куля pt)
csguy
Використовуючи наш веб-сайт, ви визнаєте, що прочитали та зрозуміли наші Політику щодо файлів cookie та Політику конфіденційності.
Licensed under cc by-sa 3.0 with attribution required.