cout源码浅析
目录
首先来看我从cplusplus中截取的这张图:
注意最下面这一行字。cout其实是ostream的一个标准对象object。而上面则演示了一些继承关系。
好的,理解了之后,接下来就去观察一下源码实现吧!
cout源码浅析
测试环境:VS2019
对于初学C++的时候,我们常常直接这样去用:
#include <iostream>
using namespace std;
int main()
{
cout << "Hello World" << endl;
}
那么理解了cout是一个object之后,我们就去查看一下它具体的实现:
在iostream头文件中,可以看到如下语句:
而该语句是包含在namespace std中的。
从这一句也可以看到cout的类型为ostream。
进一步查看前面的_CRTDATA2_IMPORT,可以看到宏定义:
#define _CRTDATA2_IMPORT _CRTIMP2_IMPORT
接着看:
#define _CRTIMP2_IMPORT __declspec(dllimport)
这里的__declspec是MSVC编译器的关键字, __declspec(dllimport)就表示这个东西是从别的DLL导入的。
这里多提一句extern(参考C++primer第5版41页):
为了支持分离式编译(separate compilation)机制,C++将声明和定义区分开来。
变量声明规定了变量的类型和名字,在这一点上定义与之相同。但是除此之外,定义还申请存储空间,也可能会为变量赋一个初始值。
如果想声明一个变量而非定义它,就在变量名前添加关键字extern,而且不要显式地初始化变量:
extern int i; // 声明i而非定义i
int j; // 声明并定义j
任何包含了显示初始化的声明即可成为定义。我们能给extern关键字标记的变量赋一个初始值,但是这么做也就抵消了extern的作用。extern语句如果包含初始值就不再是声明,而变成定义了:
extern double pi = 3.1416; // 定义
在函数体内部,如果试图初始化一个由extern关键字标记的变量,将引发错误。
变量能且只能被定义一次,但是可以被多次声明。
再来看cplusplus给出的cout注解:
着重注意这两句:
The object is declared in header<iostream>
with external linkage and static duration: it lasts the entire duration of the program.
In terms ofstatic initialization order, cout is guaranteed to be properly constructed and initialized no later than the first time an object of typeios_base::Init is constructed, with the inclusion of<iostream>counting as at least one initialization of such objects with static duration.
也就是说,其初始化早在第一次构造 ios_base::Init 类型对象初始化的时候就完成了。而其声明在iostream中有,生命周期持续到整个程序结束。
好,那让我们继续往下分析ostream。可以在iosfwd中看到如下语句:
using语义在STL中应用非常广泛。事实上早期这些都是用typedef,C++2.0之后似乎鼓励用using。
可以发现,ostream类型实际上是basic_ostream,两个模板参数:一个char,一个char_traits.
traits,萃取机,其实是一种手法,充当中间层。把一些东西丢入萃取机,然后通过一些统一的接口,去得到相应的回答。
traits各式各样,有type traits、iterator traits、char traits、allocator traits、pointer traits、array traits
关于萃取机,推荐侯捷老师的《STL源码剖析》或侯老师的课程,讲的很好。
这里可以想象,char_trairts,反应的就是character的一些特性,可以通过这个萃取机去进行询问。比如是不是宽字符呐之类的。
接着往下,思考如何通过<<去把东西输出到屏幕上呢?其实就是通过操作符重载去根据逐个的类型实现,比如说针对int型的,代码如下:
basic_ostream& __CLR_OR_THIS_CALL operator<<(int _Val) { // insert an int
ios_base::iostate _State = ios_base::goodbit;
const sentry _Ok(*this);
if (_Ok) { // state okay, use facet to insert
const _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_BEGIN
if (_Nput_fac.put(_Iter(_Myios::rdbuf()), *this, _Myios::fill(), _Tmp).failed()) {
_State |= ios_base::badbit;
}
_CATCH_IO_END
}
_Myios::setstate(_State);
return *this;
}
至此,我们已经明白了cout的大致原理:
cout是一个ostream类型的对象,ostream其实是模板类basic_ostream,其内重载了针对各种各样的type的operator<<,然后通过返回自身,使得可以连续cout,例如cout << 1 << 2,cout << 1的返回类型仍然是*this,也就是cout,接下来便又会cout << 2
定义在basic_ostream中针对的类型有int、unsigned int等等等等.
那么对于没有定义在这之中的要怎么办呢?
回顾以前我们使用string的时候,不也能直接cout一个string类型吗?
观察string源码:
同样可以看到,一个string其实就是模板类basic_string,有对应的类型、萃取机、分配器。
而重载operator<<是在类外进行的操作:
到这里我们发现了差异:
在ostream中我们在类内重载了operator<<,其中参数为所需要针对的type,比如int。
而在string的实现中(想要cout一个string类型对象),我们在类外重载了operator<<,其带有两个参数:
basic_ostream<_Elem, _Traits>& _Ostr
与 const basic_string<_Elem, _Traits, _Alloc>& _Str
如果仅考虑cout而言,可以想象,前者为cout,后者为string本身。那么为何造成这样的差异呢?
参考C++primer第5版494页,重载输出运算符<<:
通常情况下,输出运算符的第一个形参是一个非常量ostream对象的引用。之所以ostream是非常量的是因为向流写入内容会改变其状态;而该形参是引用是因为我们无法直接复制一个ostream对象。
第二个形参一般来说是一个常量的引用,该常量是我们想要打印的类类型。第二个形参是引用的原因是我们希望避免复制形参;而之所以该形参可以是常量是因为(通常情况下)打印对象不会改变对象的内容。
为了与其他输出运算符保持一致,operator<<一般要返回它的ostream形参。
因此我们可以想见,在ostream类内重载了operator<<时,成员函数默认第一个形参是this指针,因此此时我们只需要带一个int(或是别的type)型的参数即可。而在类外则必须把第一个参数给显示地指明出来。
实际使用
比如这里我自定义类,类内成员三个int的abc,输出这个类的时候想一并输出a b c:
#include <iostream>
using namespace std;
class MyData
{
public:
int a, b, c;
public:
MyData(int a, int b, int c) : a(a), b(b), c(c) {};
};
ostream& operator << (ostream& os, const MyData& my_data)
{
os << my_data.a << " " << my_data.b << " " << my_data.c;
return os;
}
int main()
{
MyData Hbh(1, 2, 3);
cout << Hbh << endl;
return 0;
}
这里类外重载operator<<时我们严格遵守C++primer所述规则:两个参数都传引用,第二个形参为常量const,最后返回ostream形参。
但是这样在类外重载有一个弊端,就是为了类外去访问,我把类MyData的类内变量都定义为public的了。
但是在类内定义的时候,由于this指针并非ostream而是这个类本身,那么就会造成第一个参数非ostream对象,怎么办呢?办法如下:
#include <iostream>
using namespace std;
class MyData
{
int a, b, c;
public:
MyData(int a, int b, int c) : a(a), b(b), c(c) {};
friend ostream& operator << (ostream& os, const MyData& my_data)
{
os << my_data.a << " " << my_data.b << " " << my_data.c;
return os;
}
};
int main()
{
MyData Hbh(1, 2, 3);
cout << Hbh << endl;
return 0;
}
即写成友元的形式。这样既不会传入默认的this指针,又可以访问类内的成员。
结语
只是兴起分析了一下cout,本人水平有限或许有纰漏,还望指正。