Tại sao iostream :: eof bên trong một điều kiện vòng lặp (nghĩa là `while (! Stream.eof ())`) bị coi là sai?


595

Tôi chỉ tìm thấy một nhận xét trong câu trả lời này nói rằng sử dụng iostream::eoftrong điều kiện vòng lặp là "gần như chắc chắn sai". Tôi thường sử dụng một cái gì đó giống như while(cin>>n)- mà tôi đoán ngầm kiểm tra EOF.

Tại sao kiểm tra eof rõ ràng sử dụng while (!cin.eof())sai?

Nó khác với việc sử dụng scanf("...",...)!=EOFtrong C (mà tôi thường sử dụng không có vấn đề gì)?


21
scanf(...) != EOFSẽ không hoạt động trong C, bởi vì scanftrả về số lượng các trường được phân tích và gán thành công. Tình trạng chính xác là scanf(...) < nở đâu nlà số lĩnh vực trong chuỗi định dạng.
Ben Voigt

5
@Ben Voigt, nó sẽ trả về một số âm (mà EOF thường được xác định như vậy) trong trường hợp đạt được EOF
Sebastian

19
@SebastianGodelet: Trên thực tế, nó sẽ trả về EOFnếu gặp phải cuối tập tin trước khi chuyển đổi trường đầu tiên (thành công hay không). Nếu kết thúc tập tin giữa các trường, nó sẽ trả về số lượng các trường được chuyển đổi và lưu trữ thành công. Mà làm cho so sánh với EOFsai.
Ben Voigt

1
@SebastianGodelet: Không, không thực sự. Anh ta sai lầm khi nói rằng "vượt qua vòng lặp không có cách nào (dễ dàng) để phân biệt đầu vào thích hợp với đầu vào không phù hợp". Trong thực tế, nó dễ dàng như kiểm tra .eof()sau khi thoát khỏi vòng lặp.
Ben Voigt

2
@Ben Có, đối với trường hợp này (đọc một int đơn giản). Nhưng người ta có thể dễ dàng đưa ra một kịch bản trong đó while(fail)vòng lặp kết thúc với cả lỗi thực tế và eof. Hãy suy nghĩ về việc nếu bạn yêu cầu 3 int mỗi lần lặp (giả sử bạn đang đọc một điểm xyz hoặc một cái gì đó), nhưng, có một cách sai lầm, chỉ có hai ints trong luồng.
sly

Câu trả lời:


544

Bởi vì iostream::eofsẽ chỉ trở lại true sau khi đọc hết luồng. Nó không chỉ ra rằng, lần đọc tiếp theo sẽ là kết thúc của luồng.

Hãy xem xét điều này (và giả sử sau đó lần đọc tiếp theo sẽ ở cuối luồng):

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
}

Chống lại điều này:

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

Và về câu hỏi thứ hai của bạn: Bởi vì

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

giống như

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

không giống như

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

12
Đáng nói là nếu (! (InStream >> data) .eof ()) cũng không làm được gì hữu ích. Fallacy 1: Nó sẽ không nhập điều kiện nếu không có khoảng trắng sau phần dữ liệu cuối cùng (mốc cuối cùng sẽ không được xử lý). Fallacy 2: Nó sẽ vào điều kiện ngay cả khi đọc dữ liệu không thành công, miễn là không đạt được EOF (vòng lặp vô hạn, xử lý cùng một dữ liệu cũ nhiều lần).
Tronic

4
Tôi nghĩ rằng nó đáng để chỉ ra rằng câu trả lời này là hơi sai lệch. Khi trích xuất ints hoặc std::strings hoặc tương tự, bit EOF được đặt khi bạn trích xuất một quyền ngay trước khi kết thúc và trích xuất chạm vào cuối. Bạn không cần phải đọc lại. Lý do nó không được đặt khi đọc từ tệp là vì có thêm \nphần cuối. Tôi đã đề cập đến điều này trong một câu trả lời khác . Đọc chars là một vấn đề khác bởi vì nó chỉ trích xuất một lần và không tiếp tục kết thúc.
Joseph Mansfield

79
Vấn đề chính là chỉ vì chúng tôi chưa đạt được EOF, không có nghĩa là lần đọc tiếp theo sẽ thành công .
Joseph Mansfield

1
@sftrabbit: tất cả đều đúng nhưng không hữu ích lắm ... ngay cả khi không có dấu vết '\ n', thật hợp lý khi muốn các khoảng trắng theo dõi khác được xử lý nhất quán với các khoảng trắng khác trong toàn bộ tệp (tức là bỏ qua). Hơn nữa, một hậu quả tinh tế của "khi bạn trích xuất một quyền ngay trước đó" là while (!eof())sẽ không "hoạt động" trên ints hoặc std::strings khi đầu vào hoàn toàn trống rỗng, vì vậy ngay cả khi biết rằng không cần \nchăm sóc theo dõi .
Tony Delroy

2
@TonyD Hoàn toàn đồng ý. Lý do tôi nói đó là vì tôi nghĩ rằng hầu hết mọi người khi họ đọc và câu trả lời tương tự sẽ nghĩ rằng nếu dòng chứa "Hello"(không dấu khoảng trắng hoặc \n) và std::stringđược chiết xuất, nó sẽ giải nén các ký tự từ Hđến o, dừng giải nén, và sau đó không đặt bit EOF. Trong thực tế, nó sẽ đặt bit EOF vì đó là EOF đã dừng trích xuất. Chỉ hy vọng để làm rõ điều đó cho mọi người.
Joseph Mansfield

103

Dòng trên cùng: Với việc xử lý đúng khoảng trắng, sau đây là cách eofsử dụng (và thậm chí, đáng tin cậy hơn so fail()với kiểm tra lỗi):

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

( Cảm ơn Tony D về gợi ý để làm nổi bật câu trả lời. Xem bình luận của anh ấy bên dưới để biết ví dụ về lý do tại sao điều này mạnh mẽ hơn. )


Đối số chính chống lại việc sử dụng eof()dường như đang thiếu một sự tinh tế quan trọng về vai trò của khoảng trắng. Đề xuất của tôi là, kiểm tra eof()rõ ràng không chỉ không phải là " luôn luôn sai " - mà dường như là một ý kiến ​​quan trọng trong các chủ đề SO tương tự này - nhưng với việc xử lý không gian trắng đúng cách, nó cung cấp cho sạch hơn và đáng tin cậy hơn xử lý lỗi và là giải pháp luôn luôn đúng (mặc dù, không nhất thiết là khó nhất).

Để tóm tắt những gì đang được đề xuất là lệnh chấm dứt và đọc "đúng" như sau:

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

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

Thất bại do nỗ lực đọc vượt quá eof được coi là điều kiện chấm dứt. Điều này có nghĩa là không có cách dễ dàng để phân biệt giữa một luồng thành công và một luồng thực sự thất bại vì những lý do khác ngoài eof. Thực hiện các luồng sau:

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

while(in>>data)chấm dứt với một bộ failbitcho cả ba đầu vào. Trong thứ nhất và thứ ba, eofbitcũng được thiết lập. Vì vậy, qua vòng lặp, người ta cần logic bổ sung rất xấu để phân biệt đầu vào thích hợp (thứ 1) với đầu vào không phù hợp (thứ 2 và thứ 3).

Trong khi đó, hãy thực hiện như sau:

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

Ở đây, in.fail()xác minh rằng miễn là có một cái gì đó để đọc, đó là một cái chính xác. Mục đích của nó không phải là một terminator vòng lặp đơn thuần.

Cho đến nay là tốt, nhưng điều gì xảy ra nếu có không gian kéo dài trong luồng - điều gì nghe có vẻ như mối quan tâm chính chống lại eof()như kẻ hủy diệt ?

Chúng tôi không cần phải từ bỏ việc xử lý lỗi của mình; Chỉ cần ăn hết khoảng trắng:

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::wsbỏ qua mọi khoảng trống tiềm năng (không hoặc nhiều hơn) trong luồng trong khi đặt eofbitkhông phải làfailbit . Vì vậy, in.fail()hoạt động như mong đợi, miễn là có ít nhất một dữ liệu để đọc. Nếu các luồng trống hoàn toàn cũng được chấp nhận, thì dạng chính xác là:

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
}

Tóm tắt: Một cấu trúc đúng while(!eof)không chỉ có thể và không sai, mà còn cho phép dữ liệu được định vị trong phạm vi và cung cấp sự phân tách kiểm tra lỗi rõ ràng hơn từ doanh nghiệp như bình thường. Điều đó đang được nói, đặc biệt while(!fail)là một thành ngữ phổ biến và ngắn gọn hơn, và có thể được ưa thích trong các tình huống đơn giản (dữ liệu trên mỗi loại đọc).


6
" Vì vậy, qua vòng lặp, không có cách (dễ dàng) để phân biệt đầu vào thích hợp với đầu vào không phù hợp. " Ngoại trừ trong một trường hợp cả hai eofbitfailbitđược đặt, chỉ có một trường hợp khác failbitđược đặt. Bạn chỉ cần kiểm tra một lần sau khi vòng lặp kết thúc, không phải trên mỗi lần lặp; nó sẽ chỉ rời khỏi vòng lặp một lần, vì vậy bạn chỉ cần kiểm tra tại sao nó lại rời khỏi vòng lặp một lần. while (in >> data)hoạt động tốt cho tất cả các luồng trống.
Jonathan Wakely

3
Những gì bạn đang nói (và một điểm được thực hiện trước đó) là một luồng có định dạng xấu có thể được xác định là !eof & failvòng lặp trong quá khứ. Có những trường hợp người ta không thể dựa vào điều này. Xem bình luận ở trên ( goo.gl/9mXYX ). Dù sao, tôi không đề xuất eof-check như là sự thay thế luôn luôn tốt hơn . Tôi chỉ đơn thuần nói, đó một cách có thể và (trong một số trường hợp thích hợp hơn) để làm điều này, thay vì "chắc chắn là sai!" vì nó có xu hướng được tuyên bố ở đây trong SO.
sly

2
"Ví dụ, hãy xem xét cách bạn muốn kiểm tra lỗi nơi dữ liệu là một struct với nhà điều hành quá tải >> đọc nhiều lĩnh vực cùng một lúc" - một trường hợp đơn giản hơn nhiều hỗ trợ quan điểm của bạn là stream >> my_intnơi mà các dòng chứa ví dụ: "-": eofbitfailbitlà bộ. Điều đó còn tệ hơn cả operator>>kịch bản, trong đó tình trạng quá tải do người dùng cung cấp ít nhất có tùy chọn xóa eofbittrước khi quay lại để hỗ trợ while (s >> x)sử dụng. Tổng quát hơn, câu trả lời này có thể sử dụng để làm sạch - chỉ có trận chung kết while( !(in>>ws).eof() )nói chung là mạnh mẽ và cuối cùng nó bị chôn vùi.
Tony Delroy

74

Bởi vì nếu lập trình viên không viết while(stream >> n), họ có thể viết điều này:

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

Vấn đề ở đây là, bạn không thể làm some work on nmà không kiểm tra trước xem luồng đã đọc có thành công hay không, vì nếu không thành công, bạn some work on nsẽ tạo ra kết quả không mong muốn.

Toàn bộ vấn đề là, eofbit, badbit, hoặc failbitđược thiết lập sau khi một nỗ lực được thực hiện để đọc từ stream. Vì vậy, nếu stream >> nthất bại, sau đó eofbit, badbithoặc failbitđược đặt ngay lập tức, vì vậy nó sẽ thành ngữ hơn nếu bạn viết while (stream >> n), bởi vì đối tượng được trả về sẽ streamchuyển thành falsenếu có một số lỗi trong việc đọc từ luồng và do đó vòng lặp dừng lại. Và nó chuyển đổi thành truenếu đọc thành công và vòng lặp tiếp tục.


1
Ngoài "kết quả không mong muốn" được đề cập với thực hiện công việc trên giá trị không xác định của n, chương trình cũng có thể rơi vào một vòng lặp vô hạn , nếu hoạt động luồng không thành công không tiêu thụ bất kỳ đầu vào nào.
mastov

10

Các câu trả lời khác đã giải thích tại sao logic sai while (!stream.eof())và cách khắc phục. Tôi muốn tập trung vào một cái gì đó khác biệt:

Tại sao kiểm tra eof rõ ràng sử dụng iostream::eofsai?

Nói chung, việc kiểm tra eof chỉ là sai vì trích xuất luồng ( >>) có thể không thành công mà không nhấn vào cuối tệp. Nếu bạn có ví dụ int n; cin >> n;và luồng chứa hello, thì đó hkhông phải là một chữ số hợp lệ, do đó quá trình trích xuất sẽ thất bại nếu không đến cuối đầu vào.

Vấn đề này, kết hợp với lỗi logic chung của việc kiểm tra trạng thái luồng trước khi cố đọc từ nó, điều đó có nghĩa là đối với N mục đầu vào, vòng lặp sẽ chạy N + 1 lần, dẫn đến các triệu chứng sau:

  • Nếu luồng trống, vòng lặp sẽ chạy một lần. >>sẽ thất bại (không có đầu vào nào được đọc) và tất cả các biến được cho là được đặt (bởi stream >> x) thực sự không được khởi tạo. Điều này dẫn đến dữ liệu rác đang được xử lý, có thể biểu hiện dưới dạng kết quả vô nghĩa (thường là số lượng rất lớn).

    (Nếu thư viện chiếu theo tiêu chuẩn của bạn để C ++ 11, mọi thứ là một chút khác nhau bây giờ: Một thất bại >>bây giờ bộ biến số để 0thay vì để họ uninitialized (trừ chars).)

  • Nếu luồng không trống, vòng lặp sẽ chạy lại sau lần nhập hợp lệ cuối cùng. Vì trong lần lặp cuối cùng, tất cả các >>hoạt động đều thất bại, các biến có khả năng giữ giá trị của chúng từ lần lặp trước. Điều này có thể biểu hiện là "dòng cuối cùng được in hai lần" hoặc "bản ghi đầu vào cuối cùng được xử lý hai lần".

    (Điều này sẽ biểu hiện một chút khác biệt kể từ C ++ 11 (xem bên trên): Bây giờ bạn nhận được "bản ghi ảo" của các số 0 thay vì một dòng cuối cùng được lặp lại.)

  • Nếu luồng chứa dữ liệu không đúng định dạng nhưng bạn chỉ kiểm tra .eof, bạn sẽ có một vòng lặp vô hạn. >>sẽ không trích xuất bất kỳ dữ liệu nào từ luồng, do đó, vòng lặp sẽ được đặt đúng chỗ mà không bao giờ kết thúc.


Tóm tắt lại: Giải pháp là kiểm tra sự thành công của >>chính hoạt động, không sử dụng một .eof()phương thức riêng biệt : while (stream >> n >> m) { ... }giống như trong C, bạn kiểm tra sự thành công của scanfchính cuộc gọi : while (scanf("%d%d", &n, &m) == 2) { ... }.


1
Đây là câu trả lời chính xác nhất, mặc dù kể từ c ++ 11, tôi không tin các biến số chưa được khởi tạo nữa (pt đầu tiên)
csguy
Khi sử dụng trang web của chúng tôi, bạn xác nhận rằng bạn đã đọc và hiểu Chính sách cookieChính sách bảo mật của chúng tôi.
Licensed under cc by-sa 3.0 with attribution required.