一、引子
std::cout << "Hello, world" << std::endl;
以上,作为任何一名接触过C++编程的朋友们来说,基本应该都是自己写下的第一行代码吧(别钻牛角尖,说什么头文件啥的)。
也许你也曾心里疑惑过,std::cout
这也不是函数的表示方式啊,为啥他能这么写?也许你的老师也跟你说过,记住这个固定句式,它就这么写!然后,没有然后。你就这样已知遵循着“谆谆教诲”,教条式的传用了整个职业生涯~
本篇,作为打破你的思维,开始刨根问题的第N次分享,我们尽量深入浅出,更多的了解下std::cout << "Hello, world" << std::endl;
,做到知其然,知其所以然。
二、鱼游浅底
2.1 cout
是什么?
extern _CRTDATA2_IMPORT ostream cout;
cout
是什么? 对,它ostream
类型的实例对象,而且是extern
修饰的全局对象。
所以,既然是实例对象,我们还可以这样使用:
使用示例
#include <iostream>int main() {std::ostream& out = std::cout; // std::cout is an instance of std::ostreamout << "Hello, World!" << std::endl;return 0;
}
2.2 cout
为何能直接 接<<
操作符 ?
对象后面 直接接 <<
操作符,这个不难联想,自然是有实现 <<
操作符。
所以,我们简单看看ostream
类
using ostream = basic_ostream<char, char_traits<char>>;
ostream
是 basic_ostream<char, char_traits<char>>
的一个类型别名。我们简单了解 下basic_ostream
类模板及其相关组件。
-
basic_ostream
类模板:basic_ostream
是 C++ 标准库中的一个类模板,用于表示输出流。它是一个通用的流类,可以与不同的字符类型和字符特性(traits)一起使用。- 模板参数包括字符类型(如
char
或wchar_t
)和字符特性类(如char_traits<char>
)。
-
char
:char
是 C++ 中的基本数据类型,用于表示单字节字符。- 在
basic_ostream<char, char_traits<char>>
中,char
指定了流处理的字符类型。
-
char_traits<char>
:char_traits
是一个特性类模板,定义了如何处理特定字符类型的操作。char_traits<char>
提供了一组静态成员函数和类型,用于描述char
类型字符的特性和操作,如比较、复制、查找等。
-
using
声明:using ostream = basic_ostream<char, char_traits<char>>;
是一种类型别名声明。- 它将
basic_ostream<char, char_traits<char>>
类型重命名为ostream
,使得代码更简洁和易读。
2.3 basic_ostream
在上面2.2 中我们已经了解到,iostream
其实只是类模版basic_ostream
特化类型basic_ostream<char, char_traits<char>>
的别名,那么回到我们最开始的问题,为何能直接使用<<
操作符连接使用,答案不言而喻,必然存在<<
的操作符函数,而且是多个函数的重载!!
说这话,我必然是有依据的。这里我为大家附上部分basic_ostream
中的代码:
basic_ostream& __CLR_OR_THIS_CALL operator<<(basic_ostream&(__cdecl* _Pfn)(basic_ostream&) ) { // call basic_ostream manipulatorreturn _Pfn(*this);}basic_ostream& __CLR_OR_THIS_CALL operator<<(_Myios&(__cdecl* _Pfn)(_Myios&) ) { // call basic_ios manipulator_Pfn(*this);return *this;}basic_ostream& __CLR_OR_THIS_CALL operator<<(ios_base&(__cdecl* _Pfn)(ios_base&) ) { // call ios_base manipulator_Pfn(*this);return *this;}basic_ostream& __CLR_OR_THIS_CALL operator<<(bool _Val) { // insert a booleanios_base::iostate _State = ios_base::goodbit;const sentry _Ok(*this);if (_Ok) { // state okay, use facet to insertconst _Nput& _Nput_fac = _STD use_facet<_Nput>(this->getloc());_TRY_IO_BEGINif (_Nput_fac.put(_Iter(_Myios::rdbuf()), *this, _Myios::fill(), _Val).failed()) {_State |= ios_base::badbit;}_CATCH_IO_END}_Myios::setstate(_State);return *this;}basic_ostream& __CLR_OR_THIS_CALL operator<<(short _Val) { // insert a shortios_base::iostate _State = ios_base::goodbit;const sentry _Ok(*this);if (_Ok) { // state okay, use facet to insertconst _Nput& _Nput_fac = _STD use_facet<_Nput>(this->getloc());ios_base::fmtflags _Bfl = this->flags() & ios_base::basefield;long _Tmp;if (_Bfl == ios_base::oct || _Bfl == ios_base::hex) {_Tmp = static_cast<long>(static_cast<unsigned short>(_Val));} else {_Tmp = static_cast<long>(_Val);}_TRY_IO_BEGINif (_Nput_fac.put(_Iter(_Myios::rdbuf()), *this, _Myios::fill(), _Tmp).failed()) {_State |= ios_base::badbit;}_CATCH_IO_END}_Myios::setstate(_State);return *this;}// NOTE:// If you are not using native wchar_t, the unsigned short inserter// is masked by an explicit specialization that treats an unsigned// short as a wide character.// To read or write unsigned shorts as integers with wchar_t streams,// make wchar_t a native type with the command line option /Zc:wchar_t.basic_ostream& __CLR_OR_THIS_CALL operator<<(unsigned short _Val) { // insert an unsigned shortios_base::iostate _State = ios_base::goodbit;const sentry _Ok(*this);if (_Ok) { // state okay, use facet to insertconst _Nput& _Nput_fac = _STD use_facet<_Nput>(this->getloc());_TRY_IO_BEGINif (_Nput_fac.put(_Iter(_Myios::rdbuf()), *this, _Myios::fill(), static_cast<unsigned long>(_Val)).failed()) {_State |= ios_base::badbit;}_CATCH_IO_END}_Myios::setstate(_State);return *this;}basic_ostream& __CLR_OR_THIS_CALL operator<<(int _Val) { // insert an intios_base::iostate _State = ios_base::goodbit;const sentry _Ok(*this);if (_Ok) { // state okay, use facet to insertconst _Nput& _Nput_fac = _STD use_facet<_Nput>(this->getloc());ios_base::fmtflags _Bfl = this->flags() & ios_base::basefield;long _Tmp;if (_Bfl == ios_base::oct || _Bfl == ios_base::hex) {_Tmp = static_cast<long>(static_cast<unsigned int>(_Val));} else {_Tmp = static_cast<long>(_Val);}_TRY_IO_BEGINif (_Nput_fac.put(_Iter(_Myios::rdbuf()), *this, _Myios::fill(), _Tmp).failed()) {_State |= ios_base::badbit;}_CATCH_IO_END}_Myios::setstate(_State);return *this;}basic_ostream& __CLR_OR_THIS_CALL operator<<(unsigned int _Val) { // insert an unsigned intios_base::iostate _State = ios_base::goodbit;const sentry _Ok(*this);if (_Ok) { // state okay, use facet to insertconst _Nput& _Nput_fac = _STD use_facet<_Nput>(this->getloc());_TRY_IO_BEGINif (_Nput_fac.put(_Iter(_Myios::rdbuf()), *this, _Myios::fill(), static_cast<unsigned long>(_Val)).failed()) {_State |= ios_base::badbit;}_CATCH_IO_END}_Myios::setstate(_State);return *this;}
basic_ostream& __CLR_OR_THIS_CALL operator<<(unsigned short _Val)
basic_ostream& __CLR_OR_THIS_CALL operator<<(int _Val)
…
这些我不必详说,当你想起自己曾经不止一次写过如下的代码:
std::cout << "我最帅" << 13 << 14 << std::endl;
你以前可能没仔细想过为啥能输出不同的数据类型,但是看到这里的朋友现在你明白了;所有发散性思维更好的朋友可能也就明白了,为啥使用 std::cout
无法打印输出某些自定义的数据类型了。
譬如:
class MyClass {
public:MyClass(const std::string& name, int value) : name_(name), value_(value) {}
private:std::string name_;int value_;
};
如何直接打印呢? MyClass myObject("Example", 42); std::cout << myObject << std::endl;
其实在我们了解了cout
的本质之后,解决方法顿上心头
。So easy!
#include <iostream>
#include <string>// Define MyClass
class MyClass {
public:MyClass(const std::string& name, int value) : name_(name), value_(value) {}// Friend function to overload the << operatorfriend std::ostream& operator<<(std::ostream& os, const MyClass& obj);private:std::string name_;int value_;
};// Overload the << operator for MyClass
std::ostream& operator<<(std::ostream& os, const MyClass& obj) {os << "MyClass(Name: " << obj.name_ << ", Value: " << obj.value_ << ")";return os;
}int main() {MyClass myObject("Example", 42);std::cout << myObject << std::endl; // This will use the overloaded operator<<return 0;
}
这个实现只能说么,太普通,大家都这么用,显示不出你的特别。这里提供另一种思路:
#include <iostream>
#include <streambuf>
#include <string>// Define MyClass
class MyClass {
public:MyClass(const std::string& name, int value) : name_(name), value_(value) {}const std::string& getName() const { return name_; }int getValue() const { return value_; }private:std::string name_;int value_;
};// Custom stream buffer that writes to std::cout
class MyStreamBuf : public std::streambuf {
protected:virtual int overflow(int c) override {if (c != EOF) {std::cout.put(static_cast<char>(c));}return c;}
};// Custom ostream class
class MyOStream : public std::ostream {
public:MyOStream() : std::ostream(&myBuf) {}// Overload << operator for MyClassMyOStream& operator<<(const MyClass& obj) {*this << "MyClass(Name: " << obj.getName() << ", Value: " << obj.getValue() << ")";return *this;}private:MyStreamBuf myBuf;
};int main() {MyClass myObject("Example", 42);MyOStream myCout;myCout << myObject << std::endl; // Use custom cout to print MyClassreturn 0;
}
三、为啥能连续使用 <<
一句话解释,<<
函数 return *this;
返回cout
对象的引用,所有你可以拆开了来理解,
std::cout << A << B << std::endl;
即:
std::cout& out_a = (std::cout << A);
std::cout& out_b = out_a << B;
所以,丝毫没有任何毛病。
四、我们讲讲 call basic_ostream manipulator
流控
以下,是我们之所以能操作流控的基础。没有下面的重载,显然是不支持 std::cout << std::endl;
语法的。
basic_ostream& __CLR_OR_THIS_CALL operator<<(basic_ostream&(__cdecl* _Pfn)(basic_ostream&) ) { // call basic_ostream manipulatorreturn _Pfn(*this);}basic_ostream& __CLR_OR_THIS_CALL operator<<(_Myios&(__cdecl* _Pfn)(_Myios&) ) { // call basic_ios manipulator_Pfn(*this);return *this;}basic_ostream& __CLR_OR_THIS_CALL operator<<(ios_base&(__cdecl* _Pfn)(ios_base&) ) { // call ios_base manipulator_Pfn(*this);return *this;}
特别说下一下两个点:
basic_ostream&(__cdecl* _Pfn)(basic_ostream&)
:- 这是一个函数指针参数,指向一个接受
basic_ostream&
参数并返回basic_ostream&
的函数。__cdecl
是一个调用约定,指定函数如何接收参数和返回值。 _Pfn
是函数指针的名称。
- 这是一个函数指针参数,指向一个接受
2 { return _Pfn(*this); }
:
- 这是函数的实现部分。它调用传入的函数指针
_Pfn
,并将当前对象(*this
)作为参数传递给该函数。 - 返回值是
_Pfn
函数的返回值,即一个basic_ostream&
引用。
作用是允许 basic_ostream
对象使用流操作符 <<
来调用特定的流操纵器(manipulator
)。流操纵器是一些函数,它们可以改变流的状态或格式化输出,例如 std::endl
、std::flush
等。
例如,当你写 std::cout << std::endl;
时,std::endl
是一个流操纵器,它被传递给这个重载的 operator<<
,从而调用 std::endl
函数来在流中插入换行符并刷新缓冲区。
流控基础知识和使用示例
这里,给出一些流控操作的相关基础知识:
基本概念
-
std::ostream
:std::cout
是std::ostream
类的一个实例。std::ostream
是一个输出流类,提供了多种方法用于将数据写入流中。
-
流缓冲区:
std::cout
使用一个流缓冲区(std::streambuf
)来暂存输出数据。数据首先被写入缓冲区,然后在适当的时候(如缓冲区满或显式刷新时)被写入到实际的输出设备(如控制台)。
-
流操纵器:
- 流操纵器是一些特殊的函数或对象,用于改变流的状态或格式化输出。例如,
std::endl
用于插入换行符并刷新缓冲区,std::setw
用于设置字段宽度。
- 流操纵器是一些特殊的函数或对象,用于改变流的状态或格式化输出。例如,
常用流操纵器
std::endl
: 插入一个换行符并刷新输出缓冲区。std::flush
: 刷新输出缓冲区,但不插入换行符。std::setw
: 设置输出字段的宽度。std::setprecision
: 设置浮点数的输出精度。std::fixed
和std::scientific
: 设置浮点数的输出格式为定点或科学计数法。
流状态
std::ios::good()
: 检查流是否处于良好状态。std::ios::eof()
: 检查流是否到达文件末尾。std::ios::fail()
: 检查流是否发生格式错误。std::ios::bad()
: 检查流是否发生不可恢复的错误。
缓冲区刷新
- 自动刷新:
std::cout
在输出换行符(\n
)时通常会自动刷新缓冲区。 - 显式刷新: 使用
std::flush
或std::endl
可以显式刷新缓冲区。
同步与线程安全
- 同步与 C 标准 I/O: 默认情况下,
std::cout
与 C 标准 I/O(如printf
)是同步的,以确保输出顺序一致。可以通过std::ios::sync_with_stdio(false)
来禁用这种同步以提高性能。 - 线程安全: C++ 标准库的流对象在多线程环境中是线程安全的,意味着多个线程可以安全地使用
std::cout
,但输出顺序可能会混乱。
使用示例
#include <iostream>
#include <iomanip>int main() {std::cout << "Hello, World!" << std::endl; // 使用 std::endl 插入换行并刷新缓冲区double pi = 3.141592653589793;std::cout << "Pi: " << std::setprecision(4) << pi << std::endl; // 设置输出精度std::cout << std::setw(10) << 42 << std::endl; // 设置字段宽度return 0;
}
五、总结
看到这里,想必对于cout
多了一些自己的理解,对于cin
来说,反之亦然。各位自行探索~
君不见,黄河之水天上来;君不见,地下暗河趣更多…
—— 南山客《新年首札》