เหตุใด Cout จึงพิมพ์“ 2 + 3 = 15” ในข้อมูลโค้ดนี้


126

เหตุใดผลลัพธ์ของโปรแกรมด้านล่างจึงเป็นอย่างไร

#include <iostream>
using namespace std;

int main(){

    cout << "2+3 = " <<
    cout << 2 + 3 << endl;
}

ผลิต

2+3 = 15

แทนที่จะเป็นที่คาดหวัง

2+3 = 5

คำถามนี้มีรอบการปิด / เปิดใหม่หลายรอบแล้ว

ก่อนที่จะปิดโหวตโปรดพิจารณาการอภิปรายเมตาเกี่ยวกับปัญหานี้


96
คุณต้องการอัฒภาคในตอนท้ายของการส่งออกบรรทัดแรกไม่ได้; <<คุณไม่ได้พิมพ์สิ่งที่คุณคิดว่าคุณกำลังพิมพ์ คุณกำลังทำcout << coutสิ่งที่พิมพ์1( cout.operator bool()ฉันคิดว่า) จากนั้น5(จาก2+3) ตามมาทันทีทำให้ดูเหมือนเลขสิบห้า
Igor Tandetnik

5
@StephanLechner นั่นอาจจะใช้ gcc4 แล้ว พวกเขาไม่มีสตรีมที่สอดคล้องกันอย่างสมบูรณ์จนกระทั่ง gcc5 โดยเฉพาะอย่างยิ่งพวกเขายังคงมีการแปลงโดยปริยายจนถึงตอนนั้น
Baum mit Augen

4
@IgorTandetnik ที่ดูเหมือนจะเป็นจุดเริ่มต้นของคำตอบ ดูเหมือนว่าจะมีรายละเอียดปลีกย่อยมากมายสำหรับคำถามนี้ซึ่งไม่ปรากฏชัดในการอ่านครั้งแรก
Mark Ransom

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

6
@jaggedSpire หากไม่ใช่ข้อผิดพลาดในการพิมพ์ก็เป็นคำถามที่แย่มากเพราะจงใจใช้โครงสร้างที่ผิดปกติซึ่งดูเหมือนเป็นข้อผิดพลาดในการพิมพ์โดยไม่ได้ชี้ให้เห็นว่านั่นเป็นความตั้งใจ ไม่ว่าจะด้วยวิธีใดก็สมควรได้รับการโหวตอย่างใกล้ชิด (เนื่องจากข้อผิดพลาดในการพิมพ์หรือไม่ดี / เป็นอันตรายนี่คือไซต์สำหรับผู้ที่ต้องการความช่วยเหลือไม่ใช่ผู้ที่พยายามหลอกลวงผู้อื่น)
David Schwartz

คำตอบ:


229

ไม่ว่าจะโดยตั้งใจหรือโดยอุบัติเหตุที่คุณมีในตอนท้ายของการส่งออกบรรทัดแรกที่คุณอาจจะหมายถึง<< ;ดังนั้นคุณจึงมี

cout << "2+3 = ";  // this, of course, prints "2+3 = "
cout << cout;      // this prints "1"
cout << 2 + 3;     // this prints "5"
cout << endl;      // this finishes the line

ดังนั้นคำถามจึงสรุปได้ว่าทำไมถึงcout << cout;พิมพ์"1"?

สิ่งนี้อาจจะเป็นเรื่องที่น่าประหลาดใจและละเอียดอ่อน std::coutผ่านคลาสพื้นฐานstd::basic_iosจัดเตรียมโอเปอเรเตอร์การแปลงบางประเภทที่มีวัตถุประสงค์เพื่อใช้ในบริบทบูลีนเช่นเดียวกับใน

while (cout) { PrintSomething(cout); }

นี่เป็นตัวอย่างที่ค่อนข้างแย่เนื่องจากเป็นการยากที่จะทำให้เอาต์พุตล้มเหลว - แต่std::basic_iosจริงๆแล้วเป็นคลาสพื้นฐานสำหรับสตรีมอินพุตและเอาต์พุตและสำหรับอินพุตจะมีความหมายมากกว่า:

int value;
while (cin >> value) { DoSomethingWith(value); }

(ออกจากลูปเมื่อสิ้นสุดสตรีมหรือเมื่ออักขระสตรีมไม่ได้สร้างจำนวนเต็มที่ถูกต้อง)

ตอนนี้คำจำกัดความที่แน่นอนของตัวดำเนินการแปลงนี้ได้เปลี่ยนไประหว่าง C ++ 03 และ C ++ 11 เวอร์ชันมาตรฐาน ในเวอร์ชันที่เก่ากว่านั้นคือoperator void*() const;(โดยทั่วไปจะใช้เป็นreturn fail() ? NULL : this;) ในขณะที่รุ่นใหม่กว่านั้นexplicit operator bool() const;(โดยทั่วไปจะใช้เป็นreturn !fail();) การประกาศทั้งสองทำงานได้ดีในบริบทบูลีน แต่ทำงานแตกต่างกันเมื่อใช้ (ผิด) นอกบริบทดังกล่าว

โดยเฉพาะอย่างยิ่งภายใต้กฎ C ++ 03 cout << coutจะถูกตีความเป็นcout << cout.operator void*()และพิมพ์ที่อยู่บางส่วน ภายใต้กฎ C ++ 11 cout << coutไม่ควรคอมไพล์เลยเนื่องจากมีการประกาศตัวดำเนินการexplicitจึงไม่สามารถเข้าร่วมในการแปลงโดยปริยายได้ นั่นเป็นแรงจูงใจหลักในการเปลี่ยนแปลงนั่นคือการป้องกันไม่ให้คอมไพล์โค้ดไร้สาระ คอมไพเลอร์ที่เป็นไปตามมาตรฐานอย่างใดอย่างหนึ่งจะไม่สร้างโปรแกรมที่พิมพ์"1"ออกมา

เห็นได้ชัดว่าการใช้งาน C ++ บางอย่างอนุญาตให้ผสมและจับคู่คอมไพเลอร์และไลบรารีในลักษณะที่ให้ผลลัพธ์ที่ไม่เป็นไปตามข้อกำหนด (อ้างถึง @StephanLechner: "ฉันพบการตั้งค่าใน xcode ซึ่งสร้าง 1 และการตั้งค่าอื่นที่ให้ที่อยู่: ภาษาถิ่น c ++ 98 รวมกับ "ไลบรารีมาตรฐาน libc ++ (ไลบรารีมาตรฐาน LLVM พร้อมการสนับสนุน c ++ 11)" ให้ 1 ในขณะที่ c ++ 98 รวมกับ libstdc (ไลบรารีมาตรฐาน gnu c ++) ให้ที่อยู่; ") คุณสามารถมีคอมไพเลอร์สไตล์ C ++ 03 ที่ไม่เข้าใจexplicitตัวดำเนินการการแปลง (ซึ่งใหม่ใน C ++ 11) รวมกับไลบรารีสไตล์ C ++ 11 ที่กำหนดการแปลงเป็นoperator bool(). ด้วยการผสมผสานดังกล่าวก็เป็นไปได้สำหรับcout << coutที่จะตีความว่าเป็นcout << cout.operator bool()ซึ่งจะเป็นเพียงและภาพพิมพ์cout << true"1"


1
@TC ฉันค่อนข้างแน่ใจว่าไม่มีความแตกต่างระหว่าง C ++ 03 และ C ++ 98 ในพื้นที่นี้ ฉันคิดว่าฉันสามารถแทนที่การกล่าวถึง C ++ 03 ทั้งหมดด้วย "pre-C ++ 11" ได้หากสิ่งนี้จะช่วยชี้แจงเรื่องต่างๆ ฉันไม่คุ้นเคยกับความซับซ้อนของคอมไพเลอร์และไลบรารีเวอร์ชันบน Linux et al; ฉันเป็นคนที่ใช้ Windows / MSVC
Igor Tandetnik

4
ฉันไม่ได้พยายาม nitpick ระหว่าง C ++ 03 และ C ++ 98; ประเด็นคือ libc ++ คือ C ++ 11 และใหม่กว่าเท่านั้น มันไม่พยายามที่จะเป็นไปตาม C ++ 98/03
TC

45

ในฐานะที่เป็นอิกอร์กล่าวว่าคุณจะได้รับนี้ด้วย C ++ 11 ห้องสมุดที่std::basic_iosมีoperator boolแทนoperator void*แต่อย่างใดไม่ได้ประกาศ explicit(หรือถือว่าเป็น) ดูที่นี่สำหรับการประกาศที่ถูกต้อง

ตัวอย่างเช่นคอมไพเลอร์ C ++ 11 ที่สอดคล้องกันจะให้ผลลัพธ์เดียวกันกับ

#include <iostream>
using namespace std;

int main() {
    cout << "2+3 = " << 
    static_cast<bool>(cout) << 2 + 3 << endl;
}

แต่ในกรณีของคุณการstatic_cast<bool>อนุญาต (ผิด) เป็นการแปลงโดยปริยาย


แก้ไข: เนื่องจากนี่ไม่ใช่พฤติกรรมปกติหรือเป็นไปตามที่คาดไว้การรู้จักแพลตฟอร์มเวอร์ชันคอมไพเลอร์ ฯลฯ อาจเป็นประโยชน์


แก้ไข 2: สำหรับการอ้างอิงรหัสมักจะเขียนเป็น

    cout << "2+3 = "
         << 2 + 3 << endl;

หรือเป็น

    cout << "2+3 = ";
    cout << 2 + 3 << endl;

และเป็นการผสมผสานทั้งสองสไตล์เข้าด้วยกันซึ่งทำให้เกิดข้อบกพร่อง


1
มีการพิมพ์ผิดในรหัสโซลูชันแรกที่แนะนำของคุณ ตัวดำเนินการหนึ่งตัวมากเกินไป
eerorika

3
ตอนนี้กำลังทำอยู่เหมือนกันต้องติดต่อกันได้ ขอบคุณ!
ไร้ประโยชน์

1
ฮา! :) ในการแก้ไขคำตอบเบื้องต้นของฉันฉันแนะนำให้เพิ่มอัฒภาค แต่ไม่ทราบตัวดำเนินการที่ท้ายบรรทัด ฉันคิดว่าร่วมกับ OP เราได้สร้างการเรียงสับเปลี่ยนที่สำคัญที่สุดของการพิมพ์ผิดนี้
eerorika

21

สาเหตุของผลลัพธ์ที่ไม่คาดคิดคือการพิมพ์ผิด คุณอาจหมายถึง

cout << "2+3 = "
     << 2 + 3 << endl;

หากเราเพิกเฉยต่อสตริงที่มีผลลัพธ์ที่คาดหวังเราจะเหลือ:

cout << cout;

เนื่องจาก C ++ 11 สิ่งนี้มีรูปแบบไม่ถูกต้อง std::coutไม่สามารถเปลี่ยนแปลงได้โดยปริยายเป็นสิ่งที่std::basic_ostream<char>::operator<<(หรือผู้ที่ไม่ใช่สมาชิกโอเวอร์โหลด) จะยอมรับ ดังนั้นคอมไพเลอร์ที่เป็นไปตามมาตรฐานอย่างน้อยต้องเตือนคุณสำหรับการทำเช่นนี้ คอมไพเลอร์ของฉันปฏิเสธที่จะรวบรวมโปรแกรมของคุณ

std::coutจะสามารถแปลงได้boolและบูลโอเวอร์โหลดของตัวดำเนินการอินพุตสตรีมจะมีเอาต์พุตที่สังเกตได้เป็น 1 อย่างไรก็ตามโอเวอร์โหลดนั้นชัดเจนดังนั้นจึงไม่ควรอนุญาตให้มีการแปลงโดยปริยาย ดูเหมือนว่าการใช้งานคอมไพเลอร์ / ไลบรารีมาตรฐานของคุณไม่เป็นไปตามมาตรฐานอย่างเคร่งครัด

ในมาตรฐานก่อน C ++ 11 สิ่งนี้ถูกสร้างขึ้นอย่างดี ย้อนกลับไปแล้วstd::coutมีตัวดำเนินการแปลงโดยปริยายvoid*ซึ่งมีตัวดำเนินการอินพุตสตรีมเกินพิกัด อย่างไรก็ตามผลลัพธ์สำหรับสิ่งนั้นจะแตกต่างกัน มันจะพิมพ์ที่อยู่หน่วยความจำของstd::coutวัตถุ


11

โค้ดที่โพสต์ไม่ควรคอมไพล์สำหรับ C ++ 11 ใด ๆ (หรือคอมไพเลอร์ที่สอดคล้องกันในภายหลัง) แต่ควรคอมไพล์โดยไม่มีคำเตือนเกี่ยวกับการใช้งาน C ++ 11 ก่อนหน้า

ความแตกต่างคือ C ++ 11 ทำให้การแปลงสตรีมเป็นบูลอย่างชัดเจน:

C.2.15 ข้อ 27: ไลบรารีอินพุต / เอาต์พุต [diff.cpp03.input.output] 27.7.2.1.3, 27.7.3.4, 27.5.5.4

การเปลี่ยนแปลง: ระบุการใช้อย่างชัดเจนในตัวดำเนินการ Conversion บูลีนที่มีอยู่
เหตุผล: ชี้แจงความตั้งใจหลีกเลี่ยงวิธีแก้ปัญหา
ผลกระทบต่อคุณลักษณะดั้งเดิม: โค้ด C ++ 2003 ที่ถูกต้องซึ่งอาศัยการแปลงบูลีนโดยปริยายจะไม่สามารถรวบรวมด้วยมาตรฐานสากลนี้ได้ การแปลงดังกล่าวเกิดขึ้นในเงื่อนไขต่อไปนี้:

  • การส่งผ่านค่าไปยังฟังก์ชันที่ใช้อาร์กิวเมนต์ประเภทบูล
    ...

ตัวดำเนินการ ostream << ถูกกำหนดด้วยพารามิเตอร์บูล เนื่องจากการแปลงเป็นบูลมีอยู่ (และไม่ชัดเจน) เป็นภาษาก่อน C ++ 11 จึงcout << coutได้รับการแปลcout << trueซึ่งให้ผล 1

และตาม C.2.15 ไม่ควรคอมไพล์โดยเริ่มต้นด้วย C ++ 11 อีกต่อไป


3
ไม่มีการแปลงเป็นboolC ++ 03 อย่างไรก็ตามมีstd::basic_ios::operator void*()ความหมายเป็นนิพจน์ควบคุมของเงื่อนไขหรือลูป
Ben Voigt

7

คุณสามารถดีบักโค้ดของคุณได้อย่างง่ายดายด้วยวิธีนี้ เมื่อคุณใช้coutเอาต์พุตของคุณจะถูกบัฟเฟอร์เพื่อให้คุณสามารถวิเคราะห์ได้ดังนี้:

ลองนึกภาพครั้งแรกที่coutแสดงถึงบัฟเฟอร์และตัวดำเนินการ<<แทนการต่อท้ายของบัฟเฟอร์ ผลการดำเนินการเป็นกระแสออกในกรณีของคุณ<< coutคุณเริ่มจาก:

cout << "2+3 = " << cout << 2 + 3 << endl;

หลังจากใช้กฎที่ระบุไว้ข้างต้นคุณจะได้รับชุดการดำเนินการดังนี้:

buffer.append("2+3 = ").append(cout).append(2 + 3).append(endl);

อย่างที่ฉันได้กล่าวไปแล้วก่อนหน้านี้ผลลัพธ์ของbuffer.append()คือบัฟเฟอร์ เมื่อเริ่มต้นบัฟเฟอร์ของคุณว่างเปล่าและคุณมีคำสั่งต่อไปนี้ที่ต้องดำเนินการ:

คำให้การ: buffer.append("2+3 = ").append(cout).append(2 + 3).append(endl);

กันชน: empty

แรกที่คุณต้องซึ่งทำให้สตริงที่กำหนดลงในบัฟเฟอร์โดยตรงและจะกลายเป็นbuffer.append("2+3 = ") bufferตอนนี้สถานะของคุณมีลักษณะดังนี้:

คำให้การ: buffer.append(cout).append(2 + 3).append(endl);

กันชน: 2+3 = 

หลังจากนั้นคุณจะวิเคราะห์คำสั่งของคุณต่อไปและคุณพบcoutว่าเป็นอาร์กิวเมนต์ที่จะผนวกท้ายบัฟเฟอร์ coutจะถือว่าเป็น1ดังนั้นคุณจะผนวก1ไปยังจุดสิ้นสุดของบัฟเฟอร์ของคุณ ตอนนี้คุณอยู่ในสถานะนี้:

คำให้การ: buffer.append(2 + 3).append(endl);

กันชน: 2+3 = 1

สิ่งต่อไปที่คุณมีในบัฟเฟอร์คือ2 + 3และเนื่องจากการเพิ่มมีลำดับความสำคัญสูงกว่าตัวดำเนินการเอาต์พุตคุณจะต้องเพิ่มตัวเลขสองตัวนี้ก่อนจากนั้นคุณจะใส่ผลลัพธ์ในบัฟเฟอร์ หลังจากนั้นคุณจะได้รับ:

คำให้การ: buffer.append(endl);

กันชน: 2+3 = 15

ในที่สุดคุณก็เพิ่มมูลค่าendlในตอนท้ายของบัฟเฟอร์และคุณมี:

คำให้การ:

กันชน: 2+3 = 15\n

หลังจากกระบวนการนี้อักขระจากบัฟเฟอร์จะถูกพิมพ์จากบัฟเฟอร์ไปยังเอาต์พุตมาตรฐานทีละตัว 2+3 = 15ดังนั้นผลของรหัสของคุณคือ หากคุณดูสิ่งนี้คุณจะได้รับเพิ่มเติม1จากที่coutคุณพยายามพิมพ์ โดยการลบออก<< coutจากคำสั่งของคุณคุณจะได้ผลลัพธ์ที่ต้องการ


6
แม้ว่านี่จะเป็นความจริงทั้งหมด (และมีรูปแบบที่สวยงาม) แต่ฉันคิดว่ามันเป็นคำถามที่น่าสงสัย ฉันเชื่อว่าคำถามนี้ทำให้"ทำไมต้องcout << coutผลิต1ตั้งแต่แรก" และคุณเพิ่งยืนยันว่ามันทำในระหว่างการสนทนาเกี่ยวกับการผูกมัดตัวดำเนินการแทรก
ไร้ประโยชน์

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