Почему iostream::eof внутри условия цикла считается неправильным?
Я только что нашел комментарий в этом ответе о том, что с помощью iostream::eof
в состоянии цикла "почти наверняка неправильно". Я вообще использую что-то вроде while(cin>>n)
- который я предполагаю, что неявно проверяет EOF, почему проверяет eof явно с помощью iostream::eof
неправильно?
Чем он отличается от использования scanf("...",...)!=EOF
в С (который я часто использую без проблем)?
7 ответов
Так как 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
Итог: при правильной обработке пробелов, вот как 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()
проверяет, что до тех пор, пока есть, что читать, оно является правильным. Это не просто терминатор цикла 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)
это бесспорна более распространенная и лаконична идиома, и может быть предпочтительным в простых (одиночных данных для каждого типа считывания) сценарии.
Потому что, если программисты не пишут 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
если чтение прошло успешно и цикл продолжается.
Другие ответы объяснили, почему логика неверна в while (!stream.eof())
и как это исправить. Я хочу сосредоточиться на чем-то другом:
почему проверка на eof явно с помощью
iostream::eof
неправильно?
В общих чертах, проверка на eof
только неправильно, потому что поток извлечения (>>
) может завершиться ошибкой, не попав в конец файла. Если у вас есть, например, int n; cin >> n;
и поток содержит hello
, затем h
не является действительной цифрой, поэтому извлечение не удастся, не достигнув конца ввода.
Эта проблема, в сочетании с общей логической ошибкой проверки состояния потока перед попыткой чтения из него, что означает, что для N входных элементов цикл будет выполняться N+1 раз, приводит к следующим признакам:
Если поток пуст, цикл будет запущен один раз.
>>
потерпит неудачу (нет входных данных для чтения) и все переменные, которые должны были быть установлены (stream >> x
) на самом деле неинициализированы. Это приводит к тому, что данные мусора обрабатываются, что может привести к бессмысленным результатам (часто огромным количествам).Если поток не пустой, цикл будет запущен снова после последнего допустимого ввода. Так как в последней итерации все
>>
операции не выполняются, переменные, скорее всего, сохранят свое значение из предыдущей итерации. Это может проявляться как "последняя строка печатается дважды" или "последняя входная запись обрабатывается дважды".Если поток содержит искаженные данные, но вы проверяете только
.eof
, вы в конечном итоге с бесконечным циклом.>>
не удастся извлечь какие-либо данные из потока, поэтому цикл вращается, даже не достигнув конца.
Напомним: решение состоит в том, чтобы проверить успех >>
сама операция, а не использовать отдельный .eof()
метод: while (stream >> n >> m) { ... }
так же, как в C вы проверяете успех scanf
назови себя: while (scanf("%d%d", &n, &m) == 2) { ... }
,
Важно помнить, что,
inFile.eof()
не становится
True
до тех пор , пока попытка чтения не удалась, потому что вы достигли конца файла. Итак, в этом примере вы получите сообщение об ошибке.
while (!inFile.eof()){
inFile >> x;
process(x);
}
Способ сделать этот цикл правильным - объединить чтение и проверку в одну операцию, например
while (inFile >> x)
process(x);
Условно,
operator>>
возвращает поток, из которого мы читаем, и логическая проверка потока возвращается, когда поток терпит неудачу (например, при достижении конца файла).
Итак, это дает нам правильную последовательность:
- читать
- проверить успешность чтения
- если и только если тест завершится успешно, обработайте то, что мы прочитали
Если вы столкнетесь с какой-либо другой проблемой, которая мешает вам правильно прочитать файл, вы не сможете
eof()
как таковой. Например, давайте посмотрим на что-то вроде этого
int x;
while (!inFile.eof()) {
inFile >> x;
process(x);
}
Давайте проследим работу приведенного выше кода на примере
- Предположим, что содержимое файла
'1', '2', '3', 'a', 'b'
. - Цикл будет правильно читать 1, 2 и 3.
- Тогда дойдет.
- Когда он попытается извлечь как int, он потерпит неудачу.
- Теперь поток находится в состоянии сбоя, пока мы не
clear
поток, все попытки чтения из него потерпят неудачу. - Но когда мы проверим eof(), он вернет , потому что мы не в конце файла, потому что он все еще ожидает чтения.
- Цикл будет продолжать пытаться читать из файла и каждый раз будет терпеть неудачу, поэтому он никогда не достигнет конца файла.
- Таким образом, цикл выше будет работать вечно.
Но если мы используем такой цикл, мы получим требуемый результат.
while (inFile >> x)
process(x);
В этом случае поток будет преобразован в
False
не только в случае конца файла, но и в случае неудачного преобразования, например
a
что мы не можем прочитать как целое число.
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!
}
}
1 while (!read.fail()) {
2 cout << ch;
3 read.get(ch);
4 }
Если вы используете строку 2 в 3 и строку 3 в 2, вы получите ch
напечатано дважды. Так что cout перед прочтением.