探究一下c++標準IO的底層實現

說明一下,我用的是g++7.1.0編譯器,標準庫源代碼也是這個版本的。

本篇文章講解c++標準IO的底層實現結構,以及cin和cout的具體實現。linux

在看本文以前,建議先看一下以前的一篇文章,至少要知道標準IO裏面各個類之間的關係:ios

c++標準輸入輸出流關係梳理c++

1. 標準IO的底層結構

經過通讀c++標準IO的源代碼,我總結出了它的底層實現結構,如圖:ide

它分爲三層結構:外部設備、緩衝區、程序,說明以下:函數

  • 外部設備是指鍵盤、屏幕、文件等物理或者邏輯設備;
  • 緩衝區是指在數據沒有同步到外部設備以前,存放數據的一塊內存;
  • 程序就是咱們代碼生成的進程了。

下面咱們首先以輸出一個字符爲例來看一下它的實現過程,這個過程是由ostream::put函數完成,下面就探究一下put函數的具體實現。ui

1.1 先探探底層實現的底
小貼士:tcc是指template cc,cc是c++實現文件的後綴,加上t表示是模板的實現,因此tcc就是一個模板的實現文件,用於跟其餘非模板的實現文件區分開來。

ostream.tcc中找到put函數的實現代碼:this

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成員函數實現以下:atom

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函數的做用就很明顯了,它有兩個分支:spa

  • 一是當緩衝區當前位置尚未寫滿的時候,就直接把字符寫到緩衝區;
  • 二是若是已經把當前緩衝區寫滿了,那麼就要作緩衝區溢出處理。

對於這兩種狀況,很明顯各個輸出類的實現方式是不同的,先拋開基本的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這個文件實際上表明的是當前打開的終端,以當前終端爲例,關係圖以下:

這樣看來,每一個程序的輸入輸出,其實接收的都是當前終端的輸入和輸出,關於這一點,就寫到這裏,再也不展開說明了。

相關文章
相關標籤/搜索