Tại sao cout in ra Hồi 2 + 3 = 15 phạm trong đoạn mã này?


126

Tại sao đầu ra của chương trình dưới đây là gì?

#include <iostream>
using namespace std;

int main(){

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

sản xuất

2+3 = 15

thay vì mong đợi

2+3 = 5

Câu hỏi này đã đi qua nhiều chu kỳ đóng / mở lại.

Trước khi bỏ phiếu để đóng, vui lòng xem xét thảo luận meta này về vấn đề này.


96
Bạn muốn có dấu chấm phẩy ;ở cuối dòng đầu ra đầu tiên, không <<. Bạn không in những gì bạn nghĩ bạn đang in. Bạn đang làm cout << cout, mà in 1(nó sử dụng cout.operator bool(), tôi nghĩ). Sau đó 5(từ 2+3) ngay lập tức theo sau, làm cho nó trông giống như số mười lăm.
Igor Tandetnik

5
@StephanLechner Có lẽ đã sử dụng gcc4 rồi. Họ không có các luồng tuân thủ đầy đủ cho đến khi gcc5, đặc biệt, họ vẫn có ẩn chuyển đổi cho đến lúc đó.
Baum mit Augen

4
@IgorTandetnik có vẻ như là bắt đầu của một câu trả lời. Dường như có rất nhiều sự tinh tế cho câu hỏi này không rõ ràng trong lần đọc đầu tiên.
Đánh dấu tiền chuộc

14
Tại sao mọi người cứ bỏ phiếu để đóng câu hỏi này? Đó không phải là "Xin vui lòng cho tôi biết có gì sai với mã này", nhưng "Tại sao mã này tạo ra đầu ra này?" Câu trả lời đầu tiên là "bạn đã mắc lỗi đánh máy", vâng, nhưng câu thứ hai yêu cầu một lời giải thích về cách trình biên dịch diễn giải mã, tại sao đó không phải là lỗi trình biên dịch và cách nhận "1" thay vì địa chỉ con trỏ.
jaggedSpire

6
@jaggedSpire Nếu đó không phải là một lỗi đánh máy, thì đó là một câu hỏi rất tệ bởi vì sau đó nó cố tình sử dụng một cấu trúc bất thường trông giống như một lỗi đánh máy mà không chỉ ra rằng đó là cố ý. Dù bằng cách nào, để xứng đáng với một cuộc bỏ phiếu chặt chẽ. (Vì một lỗi đánh máy hoặc xấu / độc hại. Đây là trang dành cho những người đang tìm kiếm sự giúp đỡ, không phải những người đang cố lừa người khác.)
David Schwartz

Câu trả lời:


229

Cho dù cố ý hay vô tình, bạn có <<ở cuối dòng đầu ra đầu tiên, nơi bạn có thể có nghĩa ;. Vì vậy, về cơ bản bạn có

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

Vì vậy, câu hỏi sôi nổi về vấn đề này: tại sao cout << cout;in "1"?

Điều này hóa ra, có lẽ đáng ngạc nhiên, tinh tế. std::cout, thông qua lớp cơ sở của nó std::basic_ios, cung cấp một toán tử chuyển đổi loại nhất định được sử dụng trong bối cảnh boolean, như trong

while (cout) { PrintSomething(cout); }

Đây là một ví dụ khá tệ, vì rất khó để đầu ra thất bại - nhưng std::basic_iosthực sự là một lớp cơ sở cho cả luồng đầu vào và đầu ra, và đối với đầu vào, nó có ý nghĩa hơn nhiều:

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

(thoát khỏi vòng lặp ở cuối luồng hoặc khi các ký tự luồng không tạo thành số nguyên hợp lệ).

Bây giờ, định nghĩa chính xác của toán tử chuyển đổi này đã thay đổi giữa các phiên bản C ++ 03 và C ++ 11 của tiêu chuẩn. Trong các phiên bản cũ hơn, nó operator void*() const;(thường được triển khai dưới dạng return fail() ? NULL : this;), trong khi ở phiên bản mới hơn explicit operator bool() const;(thường được triển khai đơn giản như return !fail();). Cả hai khai báo hoạt động tốt trong bối cảnh boolean, nhưng hành xử khác nhau khi (mis) được sử dụng bên ngoài bối cảnh đó.

Cụ thể, theo quy tắc C ++ 03, cout << coutsẽ được hiểu là cout << cout.operator void*()và in một số địa chỉ. Theo quy tắc C ++ 11, hoàn toàn cout << coutkhông nên biên dịch, vì toán tử được khai báo explicitvà do đó không thể tham gia vào các chuyển đổi ngầm định. Trên thực tế, đó là động lực chính cho sự thay đổi - ngăn chặn mã vô nghĩa biên dịch. Một trình biên dịch tuân theo một trong hai tiêu chuẩn sẽ không tạo ra một chương trình in "1".

Rõ ràng, một số triển khai C ++ nhất định cho phép trộn và kết hợp trình biên dịch và thư viện theo cách tạo ra kết quả không phù hợp (trích dẫn @StephanLechner: "Tôi đã tìm thấy một cài đặt trong xcode tạo 1 và một cài đặt khác mang lại địa chỉ: Phương ngữ ngôn ngữ c ++ 98 kết hợp với "Thư viện chuẩn libc ++ (Thư viện chuẩn LLVM có hỗ trợ c ++ 11)" mang lại 1, trong khi c ++ 98 kết hợp với libstdc (thư viện chuẩn gnu c ++) mang lại địa chỉ; "). Bạn có thể có trình biên dịch kiểu C ++ 03 không hiểu explicittoán tử chuyển đổi (mới trong C ++ 11) kết hợp với thư viện kiểu C ++ 11 xác định chuyển đổi là operator bool(). Với sự pha trộn như vậy, nó có thể cout << coutđược hiểu là cout << cout.operator bool(), mà lần lượt chỉ đơn giản là cout << truevà in "1".


1
@TC Tôi khá chắc chắn rằng không có sự khác biệt giữa C ++ 03 và C ++ 98 trong lĩnh vực cụ thể này. Tôi cho rằng tôi có thể thay thế tất cả các đề cập của C ++ 03 bằng "pre-C ++ 11", nếu điều này sẽ giúp làm rõ vấn đề. Tôi hoàn toàn không quen thuộc với sự phức tạp của trình biên dịch và phiên bản thư viện trên Linux et al; Tôi là một anh chàng Windows / MSVC.
Igor Tandetnik

4
Tôi đã không cố gắng chuyển đổi giữa C ++ 03 và C ++ 98; điểm quan trọng là libc ++ là C ++ 11 và chỉ mới hơn; nó không cố tuân thủ C ++ 98/03.
TC

45

Như Igor nói, bạn có được điều này với thư viện C ++ 11, nơi std::basic_iosoperator boolthay thế operator void*, nhưng bằng cách nào đó không được khai báo (hoặc được coi là) explicit. Xem ở đây để khai báo chính xác.

Ví dụ, trình biên dịch C ++ 11 phù hợp sẽ cho kết quả tương tự với

#include <iostream>
using namespace std;

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

nhưng trong trường hợp của bạn, việc static_cast<bool>được (sai) được cho phép như là một chuyển đổi ngầm.


Chỉnh sửa: Vì đây không phải là hành vi thông thường hoặc dự kiến, nên có thể hữu ích khi biết nền tảng, phiên bản trình biên dịch, v.v.


Chỉnh sửa 2: Để tham khảo, mã thường sẽ được viết là

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

hoặc như

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

và nó trộn lẫn hai phong cách với nhau làm lộ ra lỗi.


1
Có một lỗi đánh máy trong mã giải pháp được đề xuất đầu tiên của bạn. Một nhà điều hành quá nhiều.
eerorika

3
Bây giờ tôi cũng đang làm điều đó, nó phải truyền nhiễm. Cảm ơn!
Vô dụng

1
Hà! :) Trong lần chỉnh sửa ban đầu câu trả lời của tôi, tôi đã đề nghị thêm dấu chấm phẩy, nhưng không nhận ra toán tử ở cuối dòng. Tôi nghĩ cùng với OP, chúng tôi đã tạo ra hầu hết các hoán vị lỗi chính tả có thể có.
eerorika

21

Lý do cho đầu ra bất ngờ là một lỗi đánh máy. Bạn có thể có nghĩa là

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

Nếu chúng ta bỏ qua các chuỗi có đầu ra dự kiến, chúng ta sẽ có:

cout << cout;

Kể từ C ++ 11, điều này không đúng. std::coutkhông hoàn toàn có thể chuyển đổi thành bất cứ điều gì std::basic_ostream<char>::operator<<(hoặc quá tải không phải thành viên) sẽ chấp nhận. Do đó, một trình biên dịch tuân thủ tiêu chuẩn ít nhất phải cảnh báo bạn về việc này. Trình biên dịch của tôi từ chối biên dịch chương trình của bạn.

std::coutsẽ có thể chuyển đổi thành boolvà quá tải bool của toán tử đầu vào luồng sẽ có đầu ra quan sát là 1. Tuy nhiên, quá tải đó là rõ ràng, do đó không nên cho phép chuyển đổi ngầm định. Dường như trình biên dịch / triển khai thư viện chuẩn của bạn không tuân thủ đúng tiêu chuẩn.

Trong một tiêu chuẩn trước C ++ 11, điều này được hình thành tốt. Trước đó, std::coutcó một toán tử chuyển đổi ngầm định void*có quá tải toán tử đầu vào luồng. Tuy nhiên, đầu ra cho điều đó sẽ khác nhau. nó sẽ in địa chỉ bộ nhớ của std::coutđối tượng.


11

Mã được đăng không nên biên dịch cho bất kỳ C ++ 11 (hoặc trình biên dịch tuân thủ mới hơn), nhưng nó sẽ biên dịch mà không có cảnh báo về việc triển khai trước C ++ 11.

Sự khác biệt là C ++ 11 đã thực hiện việc hội tụ một luồng thành một bool rõ ràng:

C.2.15 Điều 27: Thư viện đầu vào / đầu ra [diff.cpp03.input.output] 27.7.2.1.3, 27.7.3.4, 27.5.5.4

Thay đổi: Chỉ định sử dụng rõ ràng trong các toán tử chuyển đổi boolean hiện tại
Đặt vấn đề: Làm rõ ý định, tránh giải pháp.
Ảnh hưởng đến tính năng gốc: Mã C ++ 2003 hợp lệ dựa trên các chuyển đổi boolean ẩn sẽ không thể biên dịch với Tiêu chuẩn quốc tế này. Chuyển đổi như vậy xảy ra trong các điều kiện sau đây:

  • truyền một giá trị cho hàm lấy tham số kiểu bool;
    ...

toán tử Ostream << được định nghĩa với tham số bool. Là một chuyển đổi thành bool tồn tại (và không rõ ràng) là tiền C ++ 11, cout << coutđã được dịch sang cout << trueđó mang lại 1.

Và theo C.2.15, điều này sẽ không còn được biên dịch bắt đầu với C ++ 11.


3
Không có chuyển đổi nào booltồn tại trong C ++ 03, tuy nhiên, std::basic_ios::operator void*()điều này có ý nghĩa như biểu thức kiểm soát của một điều kiện hoặc vòng lặp.
Ben Voigt

7

Bạn có thể dễ dàng gỡ lỗi mã của bạn theo cách này. Khi bạn sử dụng coutđầu ra của mình được đệm để bạn có thể phân tích nó như thế này:

Hãy tưởng tượng lần xuất hiện đầu tiên của coutđại diện cho bộ đệm và toán tử <<biểu thị nối thêm vào cuối bộ đệm. Kết quả của toán tử <<là luồng đầu ra, trong trường hợp của bạn cout. Bạn bắt đầu từ:

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

Sau khi áp dụng các quy tắc đã nêu ở trên, bạn nhận được một bộ hành động như thế này:

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

Như tôi đã nói trước khi kết quả buffer.append()là đệm. Lúc đầu, bộ đệm của bạn trống và bạn có câu lệnh sau để xử lý:

tuyên bố: buffer.append("2+3 = ").append(cout).append(2 + 3).append(endl);

đệm: empty

Đầu tiên bạn có buffer.append("2+3 = ")chuỗi đặt trực tiếp vào bộ đệm và trở thành buffer. Bây giờ trạng thái của bạn trông như thế này:

tuyên bố: buffer.append(cout).append(2 + 3).append(endl);

đệm: 2+3 = 

Sau đó, bạn tiếp tục phân tích tuyên bố của mình và bạn đi qua coutnhư một đối số để nối vào cuối bộ đệm. Điều coutnày được coi là như 1vậy bạn sẽ nối 1vào cuối bộ đệm của bạn. Bây giờ bạn đang ở trong trạng thái này:

tuyên bố: buffer.append(2 + 3).append(endl);

đệm: 2+3 = 1

Điều tiếp theo bạn có trong bộ đệm là 2 + 3và vì phép cộng có độ ưu tiên cao hơn toán tử đầu ra, trước tiên bạn sẽ thêm hai số này và sau đó bạn sẽ đặt kết quả vào bộ đệm. Sau đó bạn nhận được:

tuyên bố: buffer.append(endl);

đệm: 2+3 = 15

Cuối cùng, bạn thêm giá trị endlvào cuối bộ đệm và bạn có:

tuyên bố:

đệm: 2+3 = 15\n

Sau quá trình này, các ký tự từ bộ đệm được in từ bộ đệm đến đầu ra tiêu chuẩn từng cái một. Vì vậy, kết quả của mã của bạn là 2+3 = 15. Nếu bạn nhìn vào điều này, bạn sẽ nhận được thêm 1từ coutbạn đã cố gắng in. Bằng cách loại bỏ << coutkhỏi tuyên bố của bạn, bạn sẽ nhận được đầu ra mong muốn.


6
Mặc dù đây là tất cả sự thật (và được định dạng đẹp), tôi nghĩ rằng nó đang cầu xin câu hỏi. Tôi tin rằng câu hỏi sôi nổi đến "Tại sao cout << coutsản xuất 1ở nơi đầu tiên?" và bạn vừa khẳng định rằng nó đang ở giữa một cuộc thảo luận về chuỗi toán tử chèn.
Vô dụng

1
+1 cho định dạng đẹp mặc dù. Xem xét rằng đây là câu trả lời đầu tiên của bạn, thật tuyệt khi bạn đang cố gắng giúp đỡ :)
gldraphael
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.