ฉันเพิ่งพบความคิดเห็นในคำตอบนี้โดยบอกว่าการใช้iostream::eofเงื่อนไขแบบวนซ้ำนั้น "ผิดเกือบแน่นอน" โดยทั่วไปฉันใช้บางอย่างเช่นwhile(cin>>n)- ซึ่งฉันเดาว่าตรวจสอบ EOF โดยปริยาย

เหตุใดการตรวจสอบ eof อย่างชัดเจนจึงใช้while (!cin.eof())ผิด

ต่างจากการใช้scanf("...",...)!=EOFในภาษา C อย่างไร (ซึ่งผมมักใช้โดยไม่มีปัญหา)?

ตอบ

เพราะ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
}    

( ขอบคุณ 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-loop

จนถึงตอนนี้ยังดีอยู่ แต่จะเกิดอะไรขึ้นถ้ามีช่องว่างต่อท้ายในสตรีม สิ่งที่ดูเหมือนจะเป็นข้อกังวลหลัก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())และจะแก้ไขได้อย่างไร ฉันต้องการมุ่งเน้นไปที่บางสิ่งที่แตกต่าง:

why is checking for eof explicitly using iostream::eof wrong?

โดยทั่วไป การตรวจสอบeof เฉพาะจะไม่ถูกต้อง เนื่องจากการแยกสตรีม ( >>) อาจล้มเหลวโดยไม่ต้องกดที่ส่วนท้ายของไฟล์ หากคุณมี eg 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) { ... }.

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