关于c++:探究一下c标准IO的底层实现

37次阅读

共计 5684 个字符,预计需要花费 15 分钟才能阅读完成。

阐明一下,我用的是 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这个文件实际上代表的是以后关上的终端,以以后终端为例,关系图如下:

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

正文完
 0