cout源码浅析

news/2024/10/17 14:12:46/

目录

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 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;}

至此,我们已经明白了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,本人水平有限或许有纰漏,还望指正。

 

 


http://www.ppmy.cn/news/59395.html

相关文章

Python SMTP

SMTP&#xff08;简单邮件传输协议&#xff09;是电子邮件传输的标准协议。Python 提供了 smtplib 模块&#xff0c;用于在 Python 中发送电子邮件。下面是一个简单的例子&#xff1a; python import smtplib # SMTP 服务器地址和端口号 smtp_server smtp.example.com smtp_…

沁恒 CH32V208(二): CH32V208的储存结构, 启动模式和时钟

目录 沁恒 CH32V208(一): CH32V208WBU6 评估板上手报告和Win10环境配置沁恒 CH32V208(二): CH32V208的储存结构, 启动模式和时钟 CH32V 存储容量命名方式 在介绍下面的内容前, 先看一下CH32V系列和存储相关的命名格式, 以CH32V203为例, 前面的CH32V203代表一个系列, 后面的字…

什么是存储器刷新

简答 22. 什么是存储器刷新&#xff1f;常用哪几种方式&#xff1f; 为了维护所存信息&#xff0c;需要在一定时间内将所存的信息读出再重新写入&#xff0c;这一过程称为刷新。刷新是一行一行进行的&#xff0c;由CPU自动完成。 主要有&#xff1a;可集中刷新,分散刷新和异步刷…

ubuntu16.04升级到20.04后报错 By not providing “FindEigen.cmake“

编译问题&#xff1a; CMake Error at modules/perception/lidar/CMakeLists.txt:14 (find_package): By not providing "FindEigen.cmake" in CMAKE_MODULE_PATH this project has asked CMake to find a package configuration file provided by "Eigen&…

安装Node.js和cnpm

一、安装Node.js 1.下载 Node.js官网下载 根据自身系统下载对应的安装包&#xff08;我这里为Windows10 64位&#xff0c;故选择下载第一个安装包&#xff09; 2、然后点击安装&#xff0c;选择自己要安装的路径&#xff0c;此处我选择的是&#xff1a;D:\Program Files\node…

MapReduce常用参数调优

一、资源相关参数 mapred-default.xml 配置参数参数说明mapreduce.map.memory.mb一个MapTask可使用的资源上限&#xff08;单位:MB&#xff09;&#xff0c;默认为1024。如果MapTask实际使用的资源量超过该值&#xff0c;则会被强制杀死。mapreduce.reduce.memory.mb一个Redu…

为什么要通过API接口来获取数据

API接口&#xff08;应用编程接口 application/programming接口&#xff09;&#xff0c;准许应用程序通过定义的接口标准来访问另一个应用程序或服务的编程方式。简单来说&#xff0c;API就是两个软件或系统之间的通信语言或接口。 在当今的互联网时代&#xff0c;数据无处不…

你真的熟悉多线程的程序的编写?快来查漏补缺

目录 一、Thread 类的属性及常用的构造方法 1.1、 Thread 常见构造方法 1.2、Thread 类的常见属性 1.3、启动&#xff08;创建&#xff09;一个线程 1.4、中断一个线程 1.5、等待一个线程 1.6、休眠当前线程 1.7、当前线程让出的 CPU 资源 二、线程状态 一、Thread 类…