ทำไม iostream :: eof ภายในสภาพลูป (เช่น `while (! stream.eof ())`) ถือว่าผิด


595

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

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

มันแตกต่างจากการใช้scanf("...",...)!=EOFใน C (ซึ่งฉันมักจะใช้ไม่มีปัญหา)?


21
scanf(...) != EOFจะไม่ทำงานใน C เนื่องจากscanfคืนค่าจำนวนฟิลด์ที่แยกวิเคราะห์และกำหนดสำเร็จแล้ว เงื่อนไขที่ถูกต้องคือscanf(...) < nตำแหน่งที่nมีจำนวนฟิลด์ในสตริงรูปแบบ
Ben Voigt

5
@ Ben ยต์ก็จะกลับเป็นจำนวนลบ (ซึ่ง EOF มักจะถูกกำหนดให้เป็นดังกล่าว) ในกรณี EOF ถึง
เซบาสเตียน

19
@SebastianGodelet: ที่จริงแล้วมันจะกลับมาEOFถ้าพบจุดสิ้นสุดของไฟล์ก่อนการแปลงภาคสนามครั้งแรก (สำเร็จหรือไม่) หากถึงจุดสิ้นสุดไฟล์ระหว่างฟิลด์มันจะส่งคืนจำนวนฟิลด์ที่แปลงและจัดเก็บสำเร็จ ซึ่งทำให้เปรียบเทียบกับEOFผิด
Ben Voigt

1
@SebastianGodelet: ไม่ไม่ได้จริงๆ เขาทำเมื่อเขาบอกว่า "ผ่านวงไม่มีวิธีที่จะแยกแยะข้อมูลที่เหมาะสมจากคนที่ไม่เหมาะสม" ในความเป็นจริงมันเป็นเรื่องง่ายเหมือนการตรวจสอบ.eof()หลังจากออกจากวง
Ben Voigt

2
@Ben ใช่สำหรับกรณีนี้ (อ่าน int ง่าย ๆ ) แต่สามารถเกิดขึ้นได้อย่างง่ายดายกับสถานการณ์ที่while(fail)ลูปยุติลงทั้งกับความล้มเหลวที่เกิดขึ้นจริงและ eof คิดว่าถ้าคุณต้องการ 3 int ต่อการทำซ้ำ (บอกว่าคุณกำลังอ่านจุด xyz หรือบางสิ่งบางอย่าง) แต่มีผิด ints เพียงสอง ints ในกระแส
sly

คำตอบ:


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 ไม่ถึง (วนซ้ำไม่สิ้นสุดการประมวลผลข้อมูลเดิมซ้ำแล้วซ้ำอีก)
Tronic

4
ฉันคิดว่ามันคุ้มค่าที่จะชี้ให้เห็นว่าคำตอบนี้ทำให้เข้าใจผิดเล็กน้อย เมื่อทำการแยกints หรือstd::strings หรือที่คล้ายกันบิต EOF จะถูกตั้งค่าเมื่อคุณแตกไฟล์ก่อนที่จะจบ คุณไม่จำเป็นต้องอ่านอีกครั้ง เหตุผลที่ไม่ได้รับการตั้งค่าเมื่ออ่านจากไฟล์เป็นเพราะมี\nส่วนท้ายที่พิเศษ ฉันได้รับการคุ้มครองในคำตอบอื่น การอ่านcharเป็นเรื่องที่แตกต่างเพราะมันจะแยกทีละครั้งและจะไม่ตีต่อไป
Joseph Mansfield

79
ปัญหาหลักคือว่าเพียงเพราะเรายังไม่ถึง EOF ที่ไม่ได้หมายถึงการอ่านต่อไปจะประสบความสำเร็จ
โจเซฟแมนส์ฟิลด์

1
@sftrabbit: จริงทั้งหมด แต่ไม่มีประโยชน์มาก ... แม้ว่าจะไม่มีการต่อท้าย '\ n' มันก็สมเหตุสมผลที่จะต้องการช่องว่างอื่น ๆ ต่อท้ายที่จะจัดการอย่างสม่ำเสมอกับช่องว่างอื่น ๆ ตลอดทั้งไฟล์ (เช่นข้าม) นอกจากนี้ผลลัพธ์ที่ละเอียดอ่อนของ "เมื่อคุณดึงสิ่งที่ถูกต้องมาก่อน" คือสิ่งนั้นwhile (!eof())จะไม่ "ทำงาน" บนints หรือstd::strings เมื่ออินพุตว่างเปล่าทั้งหมดดังนั้นแม้จะรู้ว่าไม่มี\nความจำเป็นใด ๆ
Tony Delroy

2
@TonyD เห็นด้วยทั้งหมด เหตุผลที่ฉันบอกว่าเป็นเพราะฉันคิดว่าคนส่วนใหญ่เมื่อพวกเขาอ่านและคำตอบที่คล้ายกันจะคิดว่าถ้ากระแสมี"Hello"(ไม่มีช่องว่างต่อท้ายหรือ\n) และstd::stringสกัดเป็นมันจะแยกตัวอักษรจากHไปoหยุดแยกและ จากนั้นไม่ได้ตั้งค่าบิต EOF ที่จริงแล้วมันจะตั้ง EOF บิตเพราะมันเป็น EOF ที่หยุดการแยก เพียงแค่หวังว่าจะเคลียร์สิ่งนั้นสำหรับผู้คน
Joseph Mansfield

103

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

จนถึงตอนนี้ดีมาก แต่เกิดอะไรขึ้นถ้ามีช่องว่างต่อท้ายในสตรีม - สิ่งที่ฟังดูเป็นข้อกังวลที่สำคัญ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)ทำงานได้ดีสำหรับสตรีมที่ว่างเปล่าทั้งหมด
Jonathan Wakely

3
สิ่งที่คุณกำลังพูด (และจุดที่ทำไว้ก่อนหน้า) คือสตรีมที่มีการจัดรูปแบบไม่ดีสามารถระบุได้ว่าเป็น!eof & failลูปที่ผ่านมา มีหลายกรณีที่คนเราไม่สามารถเชื่อถือได้ ดูความคิดเห็นด้านบน ( goo.gl/9mXYX ) Eitherway ผมไม่ได้เสนอeof-Check เป็นที่เสมอดีกว่าทางเลือก ฉันแค่พูดว่ามันเป็นไปได้และ (ในบางกรณีเหมาะสมกว่า) วิธีการทำเช่นนี้มากกว่า "ผิดแน่นอนที่สุด!" เนื่องจากมีแนวโน้มที่จะได้รับการอ้างสิทธิ์ในบริเวณนี้ใน SO
sly

2
"ตัวอย่างเช่นพิจารณาว่าคุณต้องการตรวจสอบข้อผิดพลาดของข้อมูลที่เป็นโครงสร้างที่มีผู้ประกอบการมากเกินไป >> อ่านหลายเขตข้อมูลในครั้งเดียว" - กรณีที่ง่ายมากที่สนับสนุนจุดของคุณเป็นstream >> my_intที่กระแสมีเช่น "-": eofbitและfailbitมี ชุด นั่นแย่กว่าoperator>>สถานการณ์ที่ผู้ใช้งานโอเวอร์โหลดจัดเตรียมไว้อย่างน้อยมีตัวเลือกในการหักล้างeofbitก่อนส่งคืนเพื่อช่วยสนับสนุนwhile (s >> x)การใช้งาน โดยทั่วไปแล้วคำตอบนี้สามารถใช้การล้างข้อมูลได้ - เฉพาะขั้นตอนสุดท้ายเท่านั้นที่while( !(in>>ws).eof() )มีความแข็งแกร่งและถูกฝังท้ายที่สุด
Tony Delroy

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โปรแกรมอาจตกอยู่ในวงวนไม่สิ้นสุดหากการดำเนินการสตรีมที่ล้มเหลวไม่ได้ใช้อินพุตใด ๆ
mastov

10

คำตอบอื่น ๆ ได้อธิบายว่าทำไมตรรกะไม่ถูกต้องwhile (!stream.eof())และวิธีการแก้ไข ฉันต้องการเน้นสิ่งที่แตกต่าง:

เหตุใดจึงตรวจสอบว่ามีการใช้iostream::eofผิดอย่างชัดเจนหรือไม่

โดยทั่วไปการตรวจสอบeof เพียงอย่างเดียวนั้นผิดเนื่องจากการดึงข้อมูลกระแสข้อมูล ( >>) อาจล้มเหลวโดยไม่ต้องกดจุดสิ้นสุดไฟล์ หากคุณมีเช่นint n; cin >> n;และสตรีมที่มีอยู่helloแล้วhอยู่ไม่ใช่ตัวเลขที่ถูกต้องดังนั้นการดึงข้อมูลจะล้มเหลวโดยไม่ถึงจุดสิ้นสุดของอินพุต

ปัญหานี้รวมกับข้อผิดพลาดทางตรรกะทั่วไปของการตรวจสอบสถานะกระแสก่อนที่จะพยายามอ่านจากมันซึ่งหมายความว่าสำหรับรายการอินพุต N การวนซ้ำจะเรียกใช้ N + 1 ครั้งทำให้เกิดอาการต่อไปนี้:

  • หากสตรีมว่างเปล่าลูปจะทำงานหนึ่งครั้ง >>จะล้มเหลว (ไม่มีอินพุตที่จะอ่าน) และตัวแปรทั้งหมดที่ควรจะถูกตั้งค่า (โดยstream >> x) จะถูกกำหนดค่าเริ่มต้นจริง ๆ สิ่งนี้นำไปสู่การประมวลผลข้อมูลขยะซึ่งอาจแสดงให้เห็นว่าเป็นผลลัพธ์ที่ไร้สาระ (มักเป็นจำนวนมาก)

    (หากไลบรารี่มาตรฐานของคุณสอดคล้องกับ C ++ 11 ตอนนี้สิ่งต่าง ๆ จะแตกต่างกันเล็กน้อยตอนนี้ความล้มเหลวใน>>ขณะนี้จะตั้งค่าตัวแปรตัวเลขให้0แทนที่จะปล่อยให้ไม่มีการกำหนดค่าเริ่มต้น (ยกเว้นสำหรับchar)

  • หากสตรีมไม่ว่างลูปจะทำงานอีกครั้งหลังจากอินพุตที่ถูกต้องครั้งสุดท้าย ตั้งแต่ในการทำซ้ำครั้งล่าสุดการ>>ดำเนินการทั้งหมดล้มเหลวตัวแปรมีแนวโน้มที่จะเก็บค่าของพวกเขาจากการทำซ้ำก่อนหน้านี้ สิ่งนี้สามารถแสดงให้เห็นได้ว่า "บรรทัดสุดท้ายถูกพิมพ์สองครั้ง" หรือ "บันทึกอินพุตล่าสุดถูกประมวลผลสองครั้ง"

    (สิ่งนี้ควรแสดงให้เห็นแตกต่างไปเล็กน้อยตั้งแต่ C ++ 11 (ดูด้านบน): ตอนนี้คุณจะได้รับ "บันทึกภาพหลอน" เป็นศูนย์แทนที่จะเป็นบรรทัดสุดท้ายซ้ำ)

  • หากสตรีมมีข้อมูลที่มีรูปแบบไม่ถูกต้อง แต่คุณตรวจสอบเท่านั้น.eofคุณจะพบกับการวนซ้ำไม่สิ้นสุด >>จะไม่สามารถดึงข้อมูลใด ๆ จากสตรีมได้ดังนั้นลูปจะเข้าที่โดยไม่สิ้นสุด


สรุป: การแก้ปัญหาคือการทดสอบความสำเร็จของ>>การดำเนินงานของตัวเองที่จะไม่ใช้แยก.eof()วิธีการ: while (stream >> n >> m) { ... }เช่นเดียวกับใน C คุณทดสอบความสำเร็จของการที่เรียกตัวเอง:scanfwhile (scanf("%d%d", &n, &m) == 2) { ... }


1
นี่เป็นคำตอบที่ถูกต้องที่สุดถึงแม้ว่าจะเป็น c ++ 11 แต่ฉันไม่เชื่อว่าตัวแปรจะไม่ถูกกำหนดค่าเริ่มต้นอีกต่อไป (กระสุนนัดแรก)
csguy
โดยการใช้ไซต์ของเรา หมายความว่าคุณได้อ่านและทำความเข้าใจนโยบายคุกกี้และนโยบายความเป็นส่วนตัวของเราแล้ว
Licensed under cc by-sa 3.0 with attribution required.