前往小程序,Get更优阅读体验!
立即前往
首页
学习
活动
专区
工具
TVP
发布
社区首页 >专栏 >探究一下c++标准IO的底层实现(3000字长文)

探究一下c++标准IO的底层实现(3000字长文)

作者头像
cpp加油站
发布2021-05-14 11:17:33
5360
发布2021-05-14 11:17:33
举报
文章被收录于专栏:cpp加油站

说明一下,我用的是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函数的实现代码:

代码语言:javascript
复制
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成员函数实现如下:

代码语言:javascript
复制
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头文件中,定义了这样一个全局静态变量:

代码语言:javascript
复制
static ios_base::Init __ioinit;

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

代码语言:javascript
复制
  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这个文件实际上代表的是当前打开的终端,以当前终端为例,关系图如下:

这样看来,每个程序的输入输出,其实接收的都是当前终端的输入和输出,关于这一点,就写到这里,不再展开说明了。

本文参与 腾讯云自媒体同步曝光计划,分享自微信公众号。
原始发表:2021-05-07,如有侵权请联系 cloudcommunity@tencent.com 删除

本文分享自 cpp加油站 微信公众号,前往查看

如有侵权,请联系 cloudcommunity@tencent.com 删除。

本文参与 腾讯云自媒体同步曝光计划  ,欢迎热爱写作的你一起参与!

评论
登录后参与评论
0 条评论
热度
最新
推荐阅读
目录
  • 1. 标准IO的底层结构
    • 1.1 先探探底层实现的底
      • 1.2 详解标准IO底层结构
        • 1.2.1 stringbuf的底层结构
        • 1.2.2 filebuf的底层结构
        • 1.2.3 iostream的底层实现
    • 2. 标准IO全局变量cin、cout的实现
    领券
    问题归档专栏文章快讯文章归档关键词归档开发者手册归档开发者手册 Section 归档