[Mac 10.7.1 Lion Intel-based x64 gcc4.2.1 xcode4.2]
Q: c++的預設輸入輸出和c的有什麼優缺點?
A: 任何一種語言都會有自身最推薦的一套模式,因為設計者可能已經很認真考慮了。沒有必要說哪種最好,哪種很差,最好和很差都有條件限制的。不同的使用者總能找到一種最適合自身的方式。
c++的輸入輸出更物件導向化和模組化,它的操作其實蘊含著對於輸入或者輸出對象的操作,是輸出一個整形,還是一個字元,或是一個自訂對象。而c中更能感受到函數和過程化的思維,輸出沒必要告訴使用者要使用一個對象,告訴我要輸出什麼,我就給你輸出什麼。
因為c++的對象化,所以不用像printf一樣一定要傳入一個字串,而是將輸出的變數單獨寫出;這就導致有時用c++的方式寫起來更方便,而有的時候在控制複雜的輸出的時刻顯得更複雜,printf的格式字串中一個小小的格式變化即可輸出不同情況,而使用cout可能需要修改更多的代碼。
c++的對象化以及重載運算子的方式,讓輸出一個對象的具體操作可以轉移到對象內部,讓對象自身來實現具體的輸出;而printf預設沒有此功能,程式員不得不在printf的時候加上具體對象輸出邏輯或者封裝一個輸出對象資料的函數,這可能讓程式員有時不能很好地記憶到底某個對象用什麼輸出函數,而c++將介面定義好了,就是<<, 看起來更便於使用。
c語言中的printf函數的執行時採用動態解析格式字串,根據是否是%d來輸出一個整形,是否是%c來輸出一個字元的方式,可能會導致運行時效率損失;而c++的輸出方式,編譯期間即可確定輸出的是什麼,這是printf效率在有時有可能低於cout的一個因素。
c++和c語言的輸入輸出混用有時可能導致一些意想不到的問題,主要在於c++庫對於c庫的部分依賴性。所以,要特別小心,如果對於內部的機制很不清楚,最好不要隨便混用。
Q: 對於輸入輸出中的各個類,它們之間到底是什麼關係?
A: c++對於輸入輸出,有很多設計,它們共同形成了可以進行輸入輸出功能的簡潔且易擴充的類設計圖。下面將一一分析:
對於cout對象,它是ostream的一個對象,ostream的定義如下:
typedef basic_istream<char> istream;///< @isiosfwd typedef basic_ostream<char> ostream;///< @isiosfwd typedef basic_iostream<char> iostream;///< @isiosfwd
同樣,包括istream和iostream的定義。對於istream, ostream和iostream, 它們實現了輸入和輸出的具體功能(當然,它們內部還將調用其它內建函式實現最終的輸入輸出)。
basic_istream類的實現如下(部分):
template<typename _CharT, typename _Traits> class basic_istream : virtual public basic_ios<_CharT, _Traits>
__istream_type& operator>>(bool& __n) { return _M_extract(__n); } __istream_type& operator>>(short& __n); __istream_type& operator>>(unsigned short& __n) { return _M_extract(__n); } __istream_type& operator>>(int& __n); __istream_type& operator>>(unsigned int& __n) { return _M_extract(__n); } __istream_type& operator>>(long& __n) { return _M_extract(__n); }
__istream_type& get(__streambuf_type& __sb) { return this->get(__sb, this->widen('\n')); } int_type peek(); __istream_type& putback(char_type __c);
可以看出,它實現了輸入一個變數以及擷取輸入資料流最後資料、返回給輸入資料流資料等一些列操作。
如下是basic_ostream的實現(部分):
template<typename _CharT, typename _Traits> class basic_ostream : virtual public basic_ios<_CharT, _Traits>
__ostream_type& operator<<(long __n) { return _M_insert(__n); } __ostream_type& operator<<(unsigned long __n) { return _M_insert(__n); } __ostream_type& operator<<(bool __n) { return _M_insert(__n); } __ostream_type& operator<<(short __n);
__ostream_type& put(char_type __c); __ostream_type& flush();
可以看出,它實現了具體的輸出不同類型資料以及重新整理等操作。同時,也可以看到,它們均繼承了basic_ios類,下面是它的部分實現:
template<typename _CharT, typename _Traits> class basic_ios : public ios_base
void clear(iostate __state = goodbit); bool good() const { return this->rdstate() == 0; } bool eof() const { return (this->rdstate() & eofbit) != 0; } bool fail() const { return (this->rdstate() & (badbit | failbit)) != 0; } bool bad() const { return (this->rdstate() & badbit) != 0; }
可以看出,basic_ios內部實現了basic_istream和basic_ostream類共同可能需要執行的地方:緩衝區狀態等。basic_ios繼承了ios_base類,它的部分實現如下:
class ios_base
inline streamsize precision() const { return _M_precision; } inline streamsize precision(streamsize __prec) { streamsize __old = _M_precision; _M_precision = __prec; return __old; } inline streamsize width() const { return _M_width; } inline ios_base& boolalpha(ios_base& __base) { __base.setf(ios_base::boolalpha); return __base; }
它主要實現了精度控制、寬度等一些更基本的流量控制。
而對於iostream類,它繼承了istream和ostream.
template<typename _CharT, typename _Traits> class basic_iostream : public basic_istream<_CharT, _Traits>, public basic_ostream<_CharT, _Traits>
可以看出,它可以實現istream和ostream的所有功能。還有,ifstream, ofstream, fstream, istrstream, ostrstream, strstream以及strstreambase和上面各個類的繼承關係可以類似分析。
總的說來,按照輸入輸出的最終功能,將它的實現抽象出共同完成的部分以及單獨完成的部分,即為上面的設計圖。
Q:istrstream, ostrstream以及strstream,它們究竟要實現什麼?
A: 把它們和c中的sprintf, sscanf聯絡起來就理解了。它們只不過是要實現如何對記憶體中一塊所謂的字串進行拼接或者取出的操作。不過c++已經將這幾個類標記為廢棄了,類似功能的類為stringstream, istringstream, ostringstream.如下舉個例子:
因為mac下xcode對於stringstream的處理稍有bug, 下面直接使用gcc4.2.1編譯:
#include <iostream>#include <sstream>#define COUT_ENDL(str) std::cout << #str << " is " << (str) << std::endl;int main(){ std::stringstream s; int i = 128; char buf[32]; int value; s << "hello" << " " << i; s >> buf; s >> value; COUT_ENDL(buf) COUT_ENDL(value) return 0;}
儲存為stringstream.cpp, 使用g++ stringstream.cpp -o stringstream編譯,運行:
buf is hellovalue is 128
其實,很簡單,使用 << 將後面的資料存入字串中,注意是用字串格式,i是個整形依然被轉換成字串格式; >> 將資料提取出來,到後面的變數中。
Q: 感覺c++的輸入輸出的形式很多很多,而且有的地方可能會混淆一些操作,為什麼會出現這種情況?
A: 出現這個問題的原因主要在於c++使用類的概念,而且輸入輸出牽扯到較多的類繼承體系,每個類都可能有一些可以被調用的公用函數或者可使用的公開變數,導致最終使用子類進行操作的時候,同樣可以使用基類的公用函數得到一些資訊,這個可能導致可以操作輸入輸出資料流的方式極大增加。但是,不管有多少變形的方式,準系統依然是那些:輸入,輸出,查詢流狀態,設定流參數和重新整理。總而言之,不管你想封裝多少函數,c++不會阻止你,不過連結器可能阻止最終的可執行檔的產生(如果最終的可執行檔超過系統限制).
Q: 對於cout, 形如cout << endl 這種形式為什麼可以通過編譯?
A: 這種形式和cout << boolalpha也是類似的。形如如下代碼:
#include <iostream>using namespace std;int main (int argc, const char * argv[]){ bool b = true; cout << boolalpha << b << endl; return 0;}
根據重載運算子,如下代碼亦是可以的:
#include <iostream>using namespace std;int main (int argc, const char * argv[]){ bool b = true; cout.operator<<(boolalpha).operator<<(b).operator<<(endl); return 0;}
那麼boolalpha和endl到底是什麼呢?
查看endl的聲明和定義,發現如下,在basic_ostream類中:
template<typename _CharT, typename _Traits> inline basic_ostream<_CharT, _Traits>& endl(basic_ostream<_CharT, _Traits>& __os) { return flush(__os.put(__os.widen('\n'))); }
boolalpha的聲明和定義,如下在ios_base類中:
inline ios_base& boolalpha(ios_base& __base) { __base.setf(ios_base::boolalpha); return __base; }
既然如此,如下的代碼就應該可以運行:
#include <iostream>using namespace std;int main (int argc, const char * argv[]){ bool b = true; boolalpha(cout); cout << b; endl(cout); return 0;}
運行,和上面代碼效果一致。從上可以看出,boolalpha和endl是函數,它們的原型分別為:
inline ios_base& boolalpha(ios_base& __base);
template<typename _CharT, typename _Traits> inline basic_ostream<_CharT, _Traits>& endl(basic_ostream<_CharT, _Traits>& __os);
從basic_ostream類中可以找到如下函數:
__ostream_type& operator<<(__ostream_type& (*__pf)(__ostream_type&)) {// _GLIBCXX_RESOLVE_LIB_DEFECTS// DR 60. What is a formatted input function?// The inserters for manipulators are *not* formatted output functions.return __pf(*this); }__ostream_type& operator<<(ios_base& (*__pf) (ios_base&)) {// _GLIBCXX_RESOLVE_LIB_DEFECTS// DR 60. What is a formatted input function?// The inserters for manipulators are *not* formatted output functions.__pf(*this);return *this; }typedef basic_ostream<_CharT, _Traits> __ostream_type;
這樣就簡單了,cout.operator<<(boolalpha)實際上是調用了cout的operator<<(ios_base &(*__pf)(ios_base &))函數,而cout.operator<<(endl)實際上是調用了cout的operator<<(__ostream_type &(*__pf)(__ostream_type &))函數。boolalpha和endl只不過是被當做函數指標傳入當參數而已。內部將分別調用boolalpha(cout);和endl(cout);來實現最終功能。我覺得這麼設計,其一好處無非是讓使用者更分便,
到處使用<<即可,不用想著到底是重載哪個函數,內部已經為上層做好了轉換。同時,對於操作子,最終不是cout在操作它,而是操作子在操作cout.
Q: cout << "\n";到底會不會重新整理緩衝區?
A: 對於行緩衝來說,它當然會重新整理緩衝區。千萬不要認為只有cout << endl;才會強制重新整理緩衝區。如下代碼,
#include <iostream>using namespace std;int main (int argc, const char * argv[]){ cout << "hello"; cout << "ok\n"; cout << endl; cout << "thanks"; return 0;}
在中間兩條cout語句和return 0; 處打上斷點,調試運行:
運行到cout << "ok\n"時,上面的"hello"並沒有被輸出,這說明在緩衝區中;當運行到cout << endl;時,hellook\n均輸出了,這說明\n確實導致了重新整理緩衝區。當然也有可能是緩衝區太小(雖然不太可能是實際的狀態),把cout << "ok\n";改為cout << "ok";後再次調試到此,hellook並沒有輸出。 運行到return 0; 時發現thanks沒有被輸出,運行完,thanks才輸出。
當然,可以肯定的是,使用\n可能不會重新整理緩衝區,但是endl一定會重新整理緩衝區。
template<typename _CharT, typename _Traits> inline basic_ostream<_CharT, _Traits>& endl(basic_ostream<_CharT, _Traits>& __os) { return flush(__os.put(__os.widen('\n'))); }
Q: 經常使用cout的setf函數、flags函數以及使用操作子比如hex, dec等,這樣可能造成一個後果:此時輸出資料流的狀態已經不是很確定了,怎麼很好地區分它們的作用?
A: 看源碼是瞭解一個問題最直接的方式。
如下setf函數源碼:
inline fmtflags setf(fmtflags __fmtfl) { fmtflags __old = _M_flags; _M_flags |= __fmtfl; return __old; }
如下flags源碼:
inline fmtflags flags(fmtflags __fmtfl) { fmtflags __old = _M_flags; _M_flags = __fmtfl; return __old; }
其中_M_flags是ios_base的一個成員,儲存流的一些配置資訊,比如是否採用16進位輸出,是否靠左對齊等等。可以看出,setf僅僅是將一種配置資訊加入原有的配置資訊中,而flags是完全替換了原有的配置資訊。同時,也提供了unsetf和setf相搭配使用。如下是流的配置資訊:
enum _Ios_Fmtflags { _S_boolalpha = 1L << 0, _S_dec = 1L << 1, _S_fixed = 1L << 2, _S_hex = 1L << 3, _S_internal = 1L << 4, _S_left = 1L << 5, _S_oct = 1L << 6, _S_right = 1L << 7, _S_scientific = 1L << 8, _S_showbase = 1L << 9, _S_showpoint = 1L << 10, _S_showpos = 1L << 11, _S_skipws = 1L << 12, _S_unitbuf = 1L << 13, _S_uppercase = 1L << 14, _S_adjustfield = _S_left | _S_right | _S_internal, _S_basefield = _S_dec | _S_oct | _S_hex, _S_floatfield = _S_scientific | _S_fixed, _S_ios_fmtflags_end = 1L << 16 };
對於hex操作,舉個例子:
#include <iostream>using namespace std;int main (int argc, const char * argv[]){ cout.setf(ios::hex); cout << 15 << endl; return 0;}
程式輸出:
15
setf並沒有起到讓輸出資料流按照十六進位輸出的作用,而如果使用cout << hex << 15 << endl; 就能夠達到效果,為什麼呢?
先看看cout << hex實際調用的hex函數的定義:
inline ios_base& hex(ios_base& __base) { __base.setf(ios_base::hex, ios_base::basefield); return __base; }
可以看出,它和cout.setf(ios::hex);是不一致的。
看看,hex函數內部調用的setf函數的定義:
inline fmtflags setf(fmtflags __fmtfl, fmtflags __mask) { fmtflags __old = _M_flags; _M_flags &= ~__mask; _M_flags |= (__fmtfl & __mask); return __old; }_S_basefield = _S_dec | _S_oct | _S_hex,
static const fmtflags basefield = _S_basefield;可以看出, _M_flags &= ~__mask; 是將流狀態中的_S_dec, _S_oct, _S_hex標誌全部清除,然後加上特定的標誌。而cout.setf(ios::hex);的方式僅僅是將十六進位標誌加入流狀態中,但是流狀態中的十進位輸出狀態依然保留,導致依然按照十進位輸出了。
瞭解了代碼內部原理,外部不管發生了多麼神奇的現象,都顯得很單純。
Q: 對於輸入和輸出資料流,clear和sync函數的區別是什嗎?
A: clear只表示流狀態標誌的清理。
/** * @brief [Re]sets the error state. * @param state The new state flag(s) to set. * * See std::ios_base::iostate for the possible bit values. Most * users will not need to pass an argument. */ void clear(iostate __state = goodbit);
而對於sync函數或者flush函數才真正將緩衝區資料進行重新整理。
xichen
2012-5-31 11:47:56