阐明一下,我用的是g++7.1.0编译器,规范库源代码也是这个版本的。

本篇文章解说c++规范IO的底层实现构造,以及cin和cout的具体实现。

在看本文之前,倡议先看一下之前的一篇文章,至多要晓得规范IO外面各个类之间的关系:

c++规范输入输出流关系梳理

1. 规范IO的底层构造

通过通读c++规范IO的源代码,我总结出了它的底层实现构造,如图:

它分为三层构造:外部设备、缓冲区、程序,阐明如下:

  • 外部设备是指键盘、屏幕、文件等物理或者逻辑设备;
  • 缓冲区是指在数据没有同步到外部设备之前,存放数据的一块内存;
  • 程序就是咱们代码生成的过程了。

上面咱们首先以输入一个字符为例来看一下它的实现过程,这个过程是由ostream::put函数实现,上面就探索一下put函数的具体实现。

1.1 先探探底层实现的底
小贴士:tcc是指template cc,cc是c++实现文件的后缀,加上t示意是模板的实现,所以tcc就是一个模板的实现文件,用于跟其余非模板的实现文件辨别开来。

ostream.tcc中找到put函数的实现代码:

template<typename _CharT, typename _Traits>    basic_ostream<_CharT, _Traits>&    basic_ostream<_CharT, _Traits>::    put(char_type __c)    {      sentry __cerb(*this);      if (__cerb)    {      ios_base::iostate __err = ios_base::goodbit;      __try        {          const int_type __put = this->rdbuf()->sputc(__c);          if (traits_type::eq_int_type(__put, traits_type::eof()))        __err |= ios_base::badbit;        }      __catch(__cxxabiv1::__forced_unwind&)        {          this->_M_setstate(ios_base::badbit);                  __throw_exception_again;        }      __catch(...)        { this->_M_setstate(ios_base::badbit); }      if (__err)        this->setstate(__err);    }      return *this;    }

以输入一个字符为例,put函数是调用了缓冲区基类basic_streambufsputc成员函数,而sputc成员函数实现如下:

int_type      sputc(char_type __c)      {    int_type __ret;    //pptr返回一个指向缓冲区下一地位的指针,epptr返回一个指向缓冲区完结地位的指针    if (__builtin_expect(this->pptr() < this->epptr(), true))      {        *this->pptr() = __c;        //pbump是把缓冲区下一地位加1        this->pbump(1);        __ret = traits_type::to_int_type(__c);      }    else        //overflow会进行缓冲区溢出解决      __ret = this->overflow(traits_type::to_int_type(__c));    return __ret;      }

那么这样看来sputc函数的作用就很显著了,它有两个分支:

  • 一是当缓冲区以后地位还没有写满的时候,就间接把字符写到缓冲区;
  • 二是如果曾经把以后缓冲区写满了,那么就要做缓冲区溢出解决。

对于这两种状况,很显著各个输入类的实现形式是不一样的,先抛开根本的ostream不说,咱们先看一下ostringstreamofstream这两个类在实现时的异同。

对于第一点,ostringstreamofstream在实现上是一样的,都是把字符写入缓冲区并把地位向后挪动一位,并没有非凡之处。

但对于第二点,ostringstream是调用的stringbufoverflow成员函数,它是在原来缓冲区用完的状况下,从新申请一块更大的长期缓冲区,而后把源缓冲区所有的数据复制过去,把以后要输入的数据退出到新的缓冲区,而后在用这个长期缓冲区与源缓冲区进行替换,这样才把一个字符写到了源缓冲区,同时也实现了缓冲区的扩容。

ofstream是调用的filebufoverflow成员函数,该函数会检测以后是否写到了缓冲区开端,很显然对于第二点而言,既然缓冲区曾经写满,那必定是曾经写到了开端,此时会调用零碎的write函数把以后缓冲区所有内容都刷新到文件中去,而后对缓冲区指针地位等进行从新初始化,留神filebuf并没有对缓冲区进行裁减。

小贴士:很显然,对于下面第二点,调用overflow函数,是应用了c++中多态,对于streambuf::overflow,它是一个虚函数,真正的实现是在stringbuf和filebuf外面。

到这里,put函数的具体实现咱们就探索完了,大抵上也探了探规范库底层实现的底子,但咱们还是对于三层构造的实现不是那么清晰,上面就来具体的说一说。

1.2 详解规范IO底层构造
1.2.1 stringbuf的底层构造

对于istringstream、ostringstream、stringstream这三个类而言,他们都是基于stringbuf来实现缓冲区的,所以说白了他们的底层实现间接看stringbuf的底层实现就ok了,那么stringbuf是基于什么来实现缓冲区的呢。

先来看一张图,如下:

留神,这里箭头批示代表应用关系,并不是继承关系,所以我这里用了比拟通明的线,后续同理。

那么当初就很显著了,stringbuf应用的是规范库中的string来作为缓冲区,如果说读取数据的话,很显著string的大小是不会变动的,但如果是写入string的话,在结构的时候也会调用string的结构,它一开始是一个空字符串,当开始写入第一个字符的时候,默认会给string对象申请一块大小为512个字节的动态内存,后续写入,就间接写入动态内存,当512个字节写完后,就会在以后内存大小根底上乘以2,而后申请一块新的内存,再把之前的数据全副复制到新的内存中来,再在新内存的前面写入要保留的字符。

那对于stringbuf的三层构造而言,它的缓冲区就是申请的内存,外部设备就是string,在逻辑上而言,他们是两层不同的皮,但实际上就实现来讲,咱们对string申请的内存进行读写,其实就是对string进行读写,从这个角度而言,stringbuf能够说是三层构造,也能够说是两层构造,就看咱们集体怎么了解了,这里不多做探讨。

1.2.2 filebuf的底层构造

同样的,对于fstream相干类而言,它的底层实现是基于filebuf的,filebuf又比stringbuf稍显简单一些,先来看图:

filebuf在调用open函数的时候会new一块char类型的动态内存,大小为BUFSIZ,BUFSIZ是系统文件外面定义的一个专门用于缓冲区的默认size,filebuf写数据的时候,是先写到这一块动态内存中去,当写满当前,会把FILE*转换为文件描述符,而后利用write函数间接写到文件中去,再对缓冲区以后写地位进行初始化,读数据则会先把数据读到缓冲区,直到以后缓冲区全副读完,才会从新从文件再次读取,对于filebuf而言,它的缓冲区大小是固定的,不会进行裁减。

所以这里对于filebuf,缓冲区就是申请的这一块动态内存,外部设备就是文件了,filebuf不论是从逻辑上还是实现上看,它都是规范的三层构造

1.2.3 iostream的底层实现

对于istream,ostream,iostream而言,他们的缓冲区应用的是streambuf,但streambuf的构造函数是爱护类型的,所以它是没有方法间接生成一个对象的,也是能够了解的,因为streambuf既没有提供缓冲区,也没有提供一个外部设备,所以它原本也是不能间接应用的,它只是作为一个基类供stringbuffilebuf调用。

如果想应用istream,ostream,iostream,那么就须要给他们传入一个可用的缓冲区对象,例如filebuf对象,这样才是可用的,但这样还不如间接应用fstream,所以对于这三个根本模板类而言,既然不可间接应用,那就不存在两层构造还是三层构造了。

2. 规范IO全局变量cin、cout的实现

上一大节说了,iostream类是不可间接应用的,然而咱们又晓得cin是istream类型的,cout是ostream类型,而且实际上规范IO中还定义了另外两个ostream类型的cerr和clog,那么他们为什么又能够间接应用呢。

在iostream头文件中,定义了这样一个全局动态变量:

static ios_base::Init __ioinit;

ios_base::Init是一个类类型,定义在ios_base.h头文件中,它的构造函数实现如下:

  ios_base::Init::Init()  {    if (__gnu_cxx::__exchange_and_add_dispatch(&_S_refcount, 1) == 0)      {    // Standard streams default to synced with "C" operations.    _S_synced_with_stdio = true;    new (&buf_cout_sync) stdio_sync_filebuf<char>(stdout);    new (&buf_cin_sync) stdio_sync_filebuf<char>(stdin);    new (&buf_cerr_sync) stdio_sync_filebuf<char>(stderr);    // The standard streams are constructed once only and never    // destroyed.    new (&cout) ostream(&buf_cout_sync);    new (&cin) istream(&buf_cin_sync);    new (&cerr) ostream(&buf_cerr_sync);    new (&clog) ostream(&buf_cerr_sync);    cin.tie(&cout);    cerr.setf(ios_base::unitbuf);    // _GLIBCXX_RESOLVE_LIB_DEFECTS    // 455. cerr::tie() and wcerr::tie() are overspecified.    cerr.tie(&cout);#ifdef _GLIBCXX_USE_WCHAR_T    new (&buf_wcout_sync) stdio_sync_filebuf<wchar_t>(stdout);    new (&buf_wcin_sync) stdio_sync_filebuf<wchar_t>(stdin);    new (&buf_wcerr_sync) stdio_sync_filebuf<wchar_t>(stderr);    new (&wcout) wostream(&buf_wcout_sync);    new (&wcin) wistream(&buf_wcin_sync);    new (&wcerr) wostream(&buf_wcerr_sync);    new (&wclog) wostream(&buf_wcerr_sync);    wcin.tie(&wcout);    wcerr.setf(ios_base::unitbuf);    wcerr.tie(&wcout);#endif    // NB: Have to set refcount above one, so that standard    // streams are not re-initialized with uses of ios_base::Init    // besides <iostream> static object, ie just using <ios> with    // ios_base::Init objects.    __gnu_cxx::__atomic_add_dispatch(&_S_refcount, 1);      }  }

以cin为例,能够看到,实际上是在结构的时候传入了一个stdio_sync_filebuf类型的对象,那咱们晓得istream只承受streambuf类型的对象,所以能够猜测到stdio_sync_filebuf应该是继承于streambuf的,找到stdio_sync_filebuf.h头文件,看到stdio_sync_filebuf果然是继承于basic_streambuf的。

对于类stdio_sync_filebuf而言,它是不存在缓冲区的,只是它会依据传入的文件指针stdin、stdout、stderr来与外部设备键盘和屏幕扯上关系,所以对于cin而言,它是通过stdin间接从键盘进行读取,而cout则是通过stdout间接输入到屏幕。

所以从构造上而言,cin、cout、cerr、clog都是只有程序和外部设备两层构造,但还有一点纳闷,咱们依据代码,实际上他们都是关上了文件,而后对文件进行了读写,那怎么会显示在外部设备上呢。

依据操作系统的不同,规范输出和输入也是实现不同的,这里咱们以linux零碎为例,来进行阐明。

在linux中,有三个规范的输出和输入文件,别离是stdin,stdout,stderr,他们都在/dev目录下,由上一章可知,cout实际上关上了/dev/stdout这个文件,而/dev/stdout又是一个软链接,它链接的是/proc/self/fd/1这个文件,而/proc/self/fd/1又链接到了/dev/pts/0这个文件,/dev/pts/0这个文件实际上代表的是以后关上的终端,以以后终端为例,关系图如下:

这样看来,每个程序的输入输出,其实接管的都是以后终端的输出和输入,对于这一点,就写到这里,不再开展阐明了。