說明一下,我用的是g++7.1.0編譯器,標準庫源代碼也是這個版本的。
本篇文章講解c++標準IO的底層實現結構,以及cin和cout的具體實現。linux
在看本文以前,建議先看一下以前的一篇文章,至少要知道標準IO裏面各個類之間的關係:ios
經過通讀c++標準IO的源代碼,我總結出了它的底層實現結構,如圖:ide
它分爲三層結構:外部設備、緩衝區、程序,說明以下:函數
下面咱們首先以輸出一個字符爲例來看一下它的實現過程,這個過程是由ostream::put
函數完成,下面就探究一下put函數的具體實現。ui
小貼士: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_streambuf
的sputc
成員函數,而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
不說,咱們先看一下ostringstream
和ofstream
這兩個類在實現時的異同。操作系統
對於第一點,ostringstream
和ofstream
在實現上是同樣的,都是把字符寫入緩衝區並把位置向後移動一位,並無特殊之處。
但對於第二點,ostringstream
是調用的stringbuf
的overflow
成員函數,它是在原來緩衝區用完的狀況下,從新申請一塊更大的臨時緩衝區,而後把源緩衝區全部的數據複製過來,把當前要輸出的數據加入到新的緩衝區,而後在用這個臨時緩衝區與源緩衝區進行交換,這樣才把一個字符寫到了源緩衝區,同時也實現了緩衝區的擴容。
而ofstream
是調用的filebuf
的overflow
成員函數,該函數會檢測當前是否寫到了緩衝區末尾,很顯然對於第二點而言,既然緩衝區已經寫滿,那確定是已經寫到了末尾,此時會調用系統的write函數把當前緩衝區全部內容都刷新到文件中去,而後對緩衝區指針位置等進行從新初始化,注意filebuf
並無對緩衝區進行擴充。
小貼士:很顯然,對於上面第二點,調用overflow函數,是使用了c++中多態,對於streambuf::overflow,它是一個虛函數,真正的實現是在stringbuf和filebuf裏面。
到這裏,put函數的具體實現咱們就探究完了,大體上也探了探標準庫底層實現的底子,但咱們仍是對於三層結構的實現不是那麼清晰,下面就來具體的說一說。
對於istringstream、ostringstream、stringstream
這三個類而言,他們都是基於stringbuf
來實現緩衝區的,因此說白了他們的底層實現直接看stringbuf
的底層實現就ok了,那麼stringbuf
是基於什麼來實現緩衝區的呢。
先來看一張圖,以下:
注意,這裏箭頭指示表明使用關係,並非繼承關係,因此我這裏用了比較透明的線,後續同理。
那麼如今就很明顯了,stringbuf
使用的是標準庫中的string來做爲緩衝區,若是說讀取數據的話,很明顯string的大小是不會變化的,但若是是寫入string的話,在構造的時候也會調用string的構造,它一開始是一個空字符串,當開始寫入第一個字符的時候,默認會給string對象申請一塊大小爲512個字節的動態內存,後續寫入,就直接寫入動態內存,當512個字節寫完後,就會在當前內存大小基礎上乘以2,而後申請一塊新的內存,再把以前的數據所有複製到新的內存中來,再在新內存的後面寫入要保存的字符。
那對於stringbuf的三層結構而言,它的緩衝區就是申請的內存,外部設備就是string,在邏輯上而言,他們是兩層不一樣的皮,但實際上就實現來說,咱們對string申請的內存進行讀寫,其實就是對string進行讀寫,從這個角度而言,stringbuf能夠說是三層結構,也能夠說是兩層結構,就看咱們我的怎麼理解了,這裏很少作討論。
一樣的,對於fstream
相關類而言,它的底層實現是基於filebuf的,filebuf又比stringbuf稍顯複雜一些,先來看圖:
filebuf在調用open函數的時候會new一塊char類型的動態內存,大小爲BUFSIZ,BUFSIZ是系統文件裏面定義的一個專門用於緩衝區的默認size,filebuf寫數據的時候,是先寫到這一塊動態內存中去,當寫滿之後,會把FILE*轉換爲文件描述符,而後利用write函數直接寫到文件中去,再對緩衝區當前寫位置進行初始化,讀數據則會先把數據讀到緩衝區,直到當前緩衝區所有讀完,纔會從新從文件再次讀取,對於filebuf而言,它的緩衝區大小是固定的,不會進行擴充。
因此這裏對於filebuf,緩衝區就是申請的這一塊動態內存,外部設備就是文件了,filebuf不管是從邏輯上仍是實現上看,它都是標準的三層結構
對於istream,ostream,iostream
而言,他們的緩衝區使用的是streambuf
,但streambuf
的構造函數是保護類型的,因此它是沒有辦法直接生成一個對象的,也是能夠理解的,由於streambuf
既沒有提供緩衝區,也沒有提供一個外部設備,因此它原本也是不能直接使用的,它只是做爲一個基類供stringbuf
和filebuf
調用。
若是想使用istream,ostream,iostream
,那麼就須要給他們傳入一個可用的緩衝區對象,例如filebuf對象,這樣纔是可用的,但這樣還不如直接使用fstream,因此對於這三個基本模板類而言,既然不可直接使用,那就不存在兩層結構仍是三層結構了。
上一小節說了,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
這個文件實際上表明的是當前打開的終端,以當前終端爲例,關係圖以下:
這樣看來,每一個程序的輸入輸出,其實接收的都是當前終端的輸入和輸出,關於這一點,就寫到這裏,再也不展開說明了。