C++引入了ostringstream、istringstream、stringstream這三個類,要使用他們建立對象就必須包含sstream.h頭文件。html
istringstream類用於執行C++風格的串流的輸入操做。
ostringstream類用於執行C風格的串流的輸出操做。
strstream類同時能夠支持C風格的串流的輸入輸出操做。ios
istringstream類是從istream和stringstreambase派生而來,ostringstream是從ostream和 stringstreambase派生而來, stringstream則是從iostream類和stringstreambase派生而來。c++
他們的繼承關係以下圖所示:git
istringstream是由一個string對象構造而來,istringstream類從一個string對象讀取字符。
istringstream的構造函數原形以下:
istringstream::istringstream(string str);程序員
#include <iostream> #include <sstream> using namespace std; int main() { istringstream istr; istr.str("1 56.7"); //上述兩個過程能夠簡單寫成 istringstream istr("1 56.7"); cout << istr.str() << endl; int a; float b; istr >> a; cout << a << endl; istr >> b; cout << b << endl; return 0; }
上例中,構造字符串流的時候,空格會成爲字符串參數的內部分界,例子中對a,b對象的輸入"賦值"操做證實了這一點,字符串的空格成爲了整型數據與浮點型數據的分解點,利用分界獲取的方法咱們事實上完成了字符串到整型對象與浮點型對象的拆分轉換過程。github
str()成員函數的使用可讓istringstream對象返回一個string字符串(例如本例中的輸出操做(cout<<istr.str();)。正則表達式
ostringstream一樣是由一個string對象構造而來,ostringstream類向一個string插入字符。
ostringstream的構造函數原形以下:
ostringstream::ostringstream(string str);
示例代碼以下:算法
#include <iostream> #include <sstream> #include <string> #include<cstdlib> using namespace std; int main() { ostringstream ostr; //ostr.str("abc"); //若是構造的時候設置了字符串參數,那麼增加操做的時候不會從結尾開始增長,而是修改原有數據,超出的部分增加 ostr.put('d'); ostr.put('e'); ostr<<"fg"; string gstr = ostr.str(); cout<<gstr; system("pause"); }
在上例代碼中,咱們經過put()或者左移操做符能夠不斷向ostr插入單個字符或者是字符串,經過str()函數返回增加事後的完整字符串數據,但值 得注意的一點是,當構造的時候對象內已經存在字符串數據的時候,那麼增加操做的時候不會從結尾開始增長,而是修改原有數據,超出的部分增加。
[ basic_stringbuf::str :
Sets or gets the text in a string buffer without changing the write position. ]編程
對於stringstream了來講,不用我多說,你們也已經知道它是用於C++風格的字符串的輸入輸出的。
stringstream的構造函數原形以下:c#
stringstream::stringstream(string str);
示例代碼以下:
#include <iostream> #include <sstream> #include <string> #include<cstdlib> using namespace std; int main() { stringstream ostr("ccc"); ostr.put('d'); ostr.put('e'); ostr<<"fg"; string gstr = ostr.str(); cout<<gstr<<endl; char a; ostr>>a; cout<<a; system("pause"); }
除此而外,stringstream類的對象咱們還經常使用它進行string與各類內置類型數據之間的轉換。
示例代碼以下:
#include <iostream> #include <sstream> #include <string> #include<cstdlib> using namespace std; int main() { stringstream sstr; //--------int轉string----------- int a=100; string str; sstr<<a; sstr>>str; cout<<str<<endl; //--------string轉char[]-------- sstr.clear();//若是你想經過使用同一stringstream對象實現多種類型的轉換,請注意在每一次轉換以後都必須調用clear()成員函數。 string name = "colinguan"; char cname[200]; sstr<<name; sstr>>cname; cout<<cname; system("pause"); }
C++標準庫中的<sstream>提供了比ANSI C的<stdio.h>更高級的一些功能,即單純性、類型安全和可擴展性。在本文中,我將展現怎樣使用這些庫來實現安全和自動的類型轉換。 爲何要學習 若是你已習慣了<stdio.h>風格的轉換,也許你首先會問:爲何要花額外的精力來學習基於<sstream>的類型轉換呢?也許對下面一個簡單的例子的回顧可以說服你。假設你想用sprintf()函數將一個變量從int類型轉換到字符串類型。爲了正確地完成這個任務,你必須確保證目標緩衝區有足夠大空間以容納轉換完的字符串。此外,還必須使用正確的格式化符。若是使用了不正確的格式化符,會致使非預知的後果。下面是一個例子: int n=10000; chars[10]; sprintf(s,」%d」,n);// s中的內容爲「10000」 char s[10]; sprintf(s,」%f」,n);// 看!錯誤的格式化符 在這種狀況下,程序員錯誤地使用了%f格式化符來替代了%d。所以,s在調用完sprintf()後包含了一個不肯定的字符串。要是能自動推導出正確的類型,那不是更好嗎? 進入stringstream 重複利用stringstream對象 若是你打算在屢次轉換中使用同一個stringstream對象,記住再每次轉換前要使用clear()方法; 在屢次轉換中重複使用同一個stringstream(而不是每次都建立一個新的對象)對象最大的好處在於效率。stringstream對象的構造和析構函數一般是很是耗費CPU時間的。 在類型轉換中使用模板 你能夠輕鬆地定義函數模板來將一個任意的類型轉換到特定的目標類型。例如,須要將各類數字值,如int、long、double等等轉換成字符串,要使用以一個string類型和一個任意值t爲參數的to_string()函數。to_string()函數將t轉換爲字符串並寫入result中。使用str()成員函數來獲取流內部緩衝的一份拷貝: template<class T> void to_string(string & result,const T& t) { ostringstream oss;//建立一個流 oss<<t;//把值傳遞如流中 result=oss.str();//獲取轉換後的字符轉並將其寫入result to_string(s2,123);//int到string to_string(s3,true);//bool到string out_type convert(const in_value & t) { stringstream stream; stream<<t;//向流中傳值 out_type result;//這裏存儲轉換結果 stream>>result;//向result中寫入值 return result; } 這樣使用convert(): double d; string salary; string s=」12.56」; d=convert<double>(s);//d等於12.56 salary=convert<string>(9000.0);//salary等於」9000」
在過去留下來的程序代碼和純粹的C程序中,傳統的<stdio.h>形式的轉換伴隨了咱們很長的一段時間。可是,如文中所述,基於stringstream的轉換擁有類型安全和不會溢出這樣搶眼的特性,使咱們有充足得理由拋棄<stdio.h>而使用<sstream>。<sstream>庫還提供了另一個特性—可擴展性。你能夠經過重載來支持自定義類型間的轉換。 stringstream一般是用來作數據轉換的。 相比c庫的轉換,它更加安全,自動和直接。
例子一:基本數據類型轉換例子 int轉string |
#include <string> #include <sstream> #include <iostream> int main() { std::stringstream stream; std::string result; int i = 1000; stream << i; //將int輸入流 stream >> result; //從stream中抽取前面插入的int值 std::cout << result << std::endl; // print the string "1000" }
運行結果:
例子二:除了基本類型的轉換,也支持char *的轉換。
#include <sstream> #include <iostream> int main() { std::stringstream stream; char result[8] ; stream << 8888; //向stream中插入8888 stream >> result; //抽取stream中的值到result std::cout << result << std::endl; // 屏幕顯示 "8888" }
例子三:再進行屢次轉換的時候,必須調用stringstream的成員函數clear().
#include <sstream> #include <iostream> int main() { std::stringstream stream; int first, second; stream<< "456"; //插入字符串 stream >> first; //轉換成int std::cout << first << std::endl; stream.clear(); //在進行屢次轉換前,必須清除stream stream << true; //插入bool值 stream >> second; //提取出int std::cout << second << std::endl; }
本文主要考慮 x86 Linux 平臺,不考慮跨平臺的可移植性,也不考慮國際化(i18n),可是要考慮 32-bit 和 64-bit 的兼容性。本文以 stdio 指代 C 語言的 scanf/printf 系列格式化輸入輸出函數。本文注意區分「編程初學者」和「C++初學者」,兩者含義不一樣。 摘要:C++ iostream 的主要做用是讓初學者有一個方便的命令行輸入輸出試驗環境,在真實的項目中不多用到 iostream,所以沒必要把精力花在深究 iostream 的格式化與 manipulator。iostream 的設計初衷是提供一個可擴展的類型安全的 IO 機制,可是後來莫名其妙地加入了 locale 和 facet 等累贅。其整個設計複雜不堪,多重+虛擬繼承的結構也很巴洛克,性能方面幾無亮點。iostream 在實際項目中的用處很是有限,爲此投入過多學習精力實在不值。 stdio 格式化輸入輸出的缺點 1. 對編程初學者不友好 看看下面這段簡單的輸入輸出代碼。 #include <stdio.h> int main() { int i; short s; float f; double d; char name[80]; scanf("%d %hd %f %lf %s", &i, &s, &f, &d, name); printf("%d %d %f %f %s", i, s, f, d, name); } 注意到其中 輸入和輸出用的格式字符串不同。輸入 short 要用 %hd,輸出用 %d;輸入 double 要用 %lf,輸出用 %f。 輸入的參數不統一。對於 i、s、f、d 等變量,在傳入 scanf() 的時候要取地址(&),而對於 name,則不用取地址。 讀者能夠試一試如何用幾句話向剛開始學編程的初學者解釋上面兩條背後緣由(涉及到傳遞函數不定參數時的類型轉換,函數調用棧的內存佈局,指針的意義,字符數組退化爲字符指針等等),若是一開始解釋不清,只好告訴學生「這是規定」。 緩衝區溢出的危險。上面的例子在讀入 name 的時候沒有指定大小,這是用 C 語言編程的安全漏洞的主要來源。應該在一開始就強調正確的作法,避免養成錯誤的習慣。正確而安全的作法如 Bjarne Stroustrup 在《Learning Standard C++ as a New Language》所示: #include <stdio.h> int main() { const int max = 80; char name[max]; char fmt[10]; sprintf(fmt, "%%%ds", max - 1); scanf(fmt, name); printf("%s\n", name); } 這個動態構造格式化字符串的作法恐怕更難向初學者解釋。 2. 安全性(security) C 語言的安全性問題近十幾年來引發了普遍的注意,C99 增長了 snprintf() 等可以指定輸出緩衝區大小的函數,輸出方面的安全性問題已經獲得解決;輸入方面彷佛沒有太大進展,還要靠程序員本身動手。 考慮一個簡單的編程任務:從文件或標準輸入讀入一行字符串,行的長度不肯定。我發現沒有哪一個 C 語言標準庫函數能完成這個任務,除非 roll your own。 首先,gets() 是錯誤的,由於不能指定緩衝區的長度。 其次,fgets() 也有問題。它能指定緩衝區的長度,因此是安全的。可是程序必須預設一個長度的最大值,這不知足題目要求「行的長度不肯定」。另外,程序沒法判斷 fgets() 到底讀了多少個字節。爲何?考慮一個文件的內容是 9 個字節的字符串 "Chen\000Shuo",注意中間出現了 '\0' 字符,若是用 fgets() 來讀取,客戶端如何知道 "\000Shuo" 也是輸入的一部分?畢竟 strlen() 只返回 4,並且整個字符串裏沒有 '\n' 字符。 最後,能夠用 glibc 定義的 getline(3) 函數來讀取不定長的「行」。這個函數能正確處理各類狀況,不過它返回的是 malloc() 分配的內存,要求調用端本身 free()。 3. 類型安全(type-safe) 若是 printf() 的整數參數類型是 int、long 等標準類型, 那麼 printf() 的格式化字符串很容易寫。可是若是參數類型是 typedef 的類型呢? 若是你想在程序中用 printf 來打印日誌,你能一眼看出下面這些類型該用 "%d" "%ld" "%lld" 中的哪個來輸出?你的選擇是否同時兼容 32-bit 和 64-bit 平臺? clock_t。這是 clock(3) 的返回類型 dev_t。這是 mknod(3) 的參數類型 in_addr_t、in_port_t。這是 struct sockaddr_in 的成員類型 nfds_t。這是 poll(2) 的參數類型 off_t。這是 lseek(2) 的參數類型,麻煩的是,這個類型與宏定義 _FILE_OFFSET_BITS 有關。 pid_t、uid_t、gid_t。這是 getpid(2) getuid(2) getgid(2) 的返回類型 ptrdiff_t。printf() 專門定義了 "t" 前綴來支持這一類型(即便用 "%td" 來打印)。 size_t、ssize_t。這兩個類型處處都在用。printf() 爲此專門定義了 "z" 前綴來支持這兩個類型(即便用 "%zu" 或 "%zd" 來打印)。 socklen_t。這是 bind(2) 和 connect(2) 的參數類型 time_t。這是 time(2) 的返回類型,也是 gettimeofday(2) 和 clock_gettime(2) 的輸出結構體的成員類型 若是在 C 程序裏要正確打印以上類型的整數,恐怕要費一番腦筋。《The Linux Programming Interface》的做者建議(3.6.2節)先統一轉換爲 long 類型再用 "%ld" 來打印;對於某些類型仍然須要特殊處理,好比 off_t 的類型多是 long long。 還有,int64_t 在 32-bit 和 64-bit 平臺上是不一樣的類型,爲此,若是程序要打印 int64_t 變量,須要包含 <inttypes.h> 頭文件,而且使用 PRId64 宏: #include <stdio.h> #define __STDC_FORMAT_MACROS #include <inttypes.h> int main() { int64_t x = 100; printf("%" PRId64 "\n", x); printf("%06" PRId64 "\n", x); } muduo 的 Timestamp 使用了 PRId64 http://code.google.com/p/muduo/source/browse/trunk/muduo/base/Timestamp.cc#25 Google C++ 編碼規範也提到了 64-bit 兼容性: http://google-styleguide.googlecode.com/svn/trunk/cppguide.xml#64-bit_Portability 這些問題在 C++ 裏都不存在,在這方面 iostream 是個進步。 C stdio 在類型安全方面本來還有一個缺點,即格式化字符串與參數類型不匹配會形成難以發現的 bug,不過如今的編譯器已經可以檢測不少這種錯誤: int main() { double d = 100.0; // warning: format '%d' expects type 'int', but argument 2 has type 'double' printf("%d\n", d); short s; // warning: format '%d' expects type 'int*', but argument 2 has type 'short int*' scanf("%d", &s); size_t sz = 1; // no warning printf("%zd\n", sz); } 4. 不可擴展? C stdio 的另一個缺點是沒法支持自定義的類型,好比我寫了一個 Date class,我沒法像打印 int 那樣用 printf 來直接打印 Date 對象。 struct Date { int year, month, day; }; Date date; printf("%D", &date); // WRONG Glibc 放寬了這個限制,容許用戶調用 register_printf_function(3) 註冊本身的類型,固然,前提是與現有的格式字符不衝突(這其實大大限制了這個功能的用處,現實中也幾乎沒有人真的去用它)。 http://www.gnu.org/s/hello/manual/libc/Printf-Extension-Example.html http://en.wikipedia.org/wiki/Printf#Custom_format_placeholders 5. 性能 C stdio 的性能方面有兩個弱點。 使用一種 little language (如今流行叫 DSL)來配置格式。當然有利於緊湊性和靈活性,但損失了一點點效率。每次打印一個整數都要先解析 "%d" 字符串,大多數狀況下不是問題,某些場合須要本身寫整數到字符串的轉換。 C locale 的負擔。locale 指的是不一樣語種對「什麼是空白」、「什麼是字母」,「什麼是小數點」有不一樣的定義(德語裏邊小數點是逗號,不是句點)。C 語言的 printf()、scanf()、isspace()、isalpha()、ispunct()、strtod() 等等函數都和 locale 有關,並且能夠在運行時動態更改。就算是程序只使用默認的 "C" locale,任然要爲這個靈活性付出代價。 iostream 的設計初衷 iostream 的設計初衷包括克服 C stdio 的缺點,提供一個高效的可擴展的類型安全的 IO 機制。「可擴展」有兩層意思,一是能夠擴展到用戶自定義類型,而是經過繼承 iostream 來定義本身的 stream,本文把前一種稱爲「類型可擴展」後一種稱爲「功能可擴展」。 「類型可擴展」和「類型安全」都是經過函數重載來實現的。 iostream 對初學者很友好,用 iostream 重寫與前面一樣功能的代碼: #include <iostream> #include <string> using namespace std; int main() { int i; short s; float f; double d; string name; cin >> i >> s >> f >> d >> name; cout << i << " " << s << " " << f << " " << d << " " << name << endl; } 這段代碼恐怕比 scanf/printf 版本容易解釋得多,並且沒有安全性(security)方面的問題。 咱們本身的類型也能夠融入 iostream,使用起來與 built-in 類型沒有區別。這主要得力於 C++ 能夠定義 non-member functions/operators。 #include <ostream> // 是否是過重量級了? class Date { public: Date(int year, int month, int day) : year_(year), month_(month), day_(day) { } void writeTo(std::ostream& os) const { os << year_ << '-' << month_ << '-' << day_; } private: int year_, month_, day_; }; std::ostream& operator<<(std::ostream& os, const Date& date) { date.writeTo(os); return os; } int main() { Date date(2011, 4, 3); std::cout << date << std::endl; // 輸出 2011-4-3 } iostream 憑藉這兩點(類型安全和類型可擴展),基本克服了 stdio 在使用上的不便與不安全。若是 iostream 止步於此,那它將是一個很是便利的庫,惋惜它前進了另一步。 iostream 與標準庫其餘組件的交互 不一樣於標準庫其餘 class 的「值語意」,iostream 是「對象語意」,即 iostream 是 non-copyable。這是正確的,由於若是 fstream 表明一個文件的話,拷貝一個 fstream 對象意味着什麼呢?表示打開了兩個文件嗎?若是銷燬一個 fstream 對象,它會關閉文件句柄,那麼另外一個 fstream copy 對象會所以受影響嗎? C++ 同時支持「數據抽象」和「面向對象編程」,其實主要就是「值語意」與「對象語意」的區別,我發現不是每一個人都清楚這一點,這裏多說幾句。標準庫裏的 complex<> 、pair<>、vector<>、 string 等等都是值語意,拷貝以後就與原對象脫離關係,就跟拷貝一個 int 同樣。而咱們本身寫的 Employee class、TcpConnection class 一般是對象語意,拷貝一個 Employee 對象是沒有意義的,一個僱員不會變成兩個僱員,他也不會領兩份薪水。拷貝 TcpConnection 對象也沒有意義,系統裏邊只有一個 TCP 鏈接,拷貝 TcpConnection 對象不會讓咱們擁有兩個鏈接。所以若是在 C++ 裏作面向對象編程,寫的 class 一般應該禁用 copy constructor 和 assignment operator,好比能夠繼承 boost::noncopyable。對象語意的類型不能直接做爲標準容器庫的成員。另外一方面,若是要寫一個圖形程序,其中用到三維空間的向量,那麼咱們能夠寫 Vector3D class,它應該是值語意的,容許拷貝,而且能夠用做標準容器庫的成員,例如 vector<Vector3D> 表示一條三維的折線。 C stdio 的另一個缺點是 FILE* 能夠隨意拷貝,可是隻要關閉其中一個 copy,其餘 copies 也都失效了,跟空懸指針通常。這其實不光是 C stdio 的缺點,整個 C 語言對待資源(malloc 獲得的內存,open() 打開的文件,socket() 打開的鏈接)都是這樣,用整數或指針來表明(即「句柄」)。而整數和指針類型的「句柄」是能夠隨意拷貝的,很容易就形成重複釋放、遺漏釋放、使用已經釋放的資源等等常見錯誤。這是由於 C 語言錯誤地讓「對象語言」的東西變成了值語意。 iostream 禁止拷貝,利用對象的生命期來明確管理資源(如文件),很天然地就避免了 C 語言易犯的錯誤。這就是 RAII,一種重要且獨特的 C++ 編程手法。 std::string iostream 能夠與 string 配合得很好。可是有一個問題:誰依賴誰? std::string 的 operator << 和 operator >> 是如何聲明的?"string" 頭文件在聲明這兩個 operators 的時候要不要 include "iostream" ? iostream 和 string 均可以單獨 include 來使用,顯然 iostream 頭文件裏不會定義 string 的 << 和 >> 操做。可是,若是"string"要include "iostream",豈不是讓 string 的用戶被迫也用了 iostream?編譯 iostream 頭文件但是至關的慢啊(由於 iostream 是 template,其實現代碼都放到了頭文件中)。 標準庫的解決辦法是定義 iosfwd 頭文件,其中包含 istream 和 ostream 等的前向聲明 (forward declarations),這樣 "string" 頭文件在定義輸入輸出操做符時就能夠沒必要包含 "iostream",只須要包含簡短得多的 "iosfwd"。咱們本身寫程序也可藉此學習如何支持可選的功能。 值得注意的是,istream::getline() 成員函數的參數類型是 char*,由於 "istream" 沒有包含 "string",而咱們經常使用的 std::getline() 函數是個 non-member function,定義在 "string" 裏邊。 std::complex 標準庫的複數類 complex 的狀況比較複雜。使用 complex 會自動包含 sstream,後者會包含 istream 和 ostream,這是個不小的負擔。問題是,爲何? 它的 operator >> 操做比 string 複雜得多,如何應對格式不正確的狀況?輸入字符串不會遇到格式不正確,可是輸入一個複數可能遇到各類問題,好比數字的格式不對等。我懷疑有誰會真的在產品項目裏用 operator >> 來讀入字符方式表示的複數,這樣的代碼的健壯性如何保證。基於一樣的理由,我認爲產品代碼中應該避免用 istream 來讀取帶格式的內容,後面也再也不談 istream 的缺點,它已經被秒殺。 它的 operator << 也很奇怪,它不是直接使用參數 ostream& os 對象來輸出,而是先構造 ostringstream,輸出到該 string stream,再把結果字符串輸出到 ostream。簡化後的代碼以下: template<typename T> std::ostream& operator<<(std::ostream& os, const std::complex<T>& x) { std::ostringstream s; s << '(' << x.real() << ',' << x.imag() << ')'; return os << s.str(); } 注意到 ostringstream 會用到動態分配內存,也就是說,每輸出一個 complex 對象就會分配釋放一次內存,效率堪憂。 根據以上分析,我認爲 iostream 和 complex 配合得很差,可是它們耦合得更緊密(與 string/iostream 相比),這多是個不得已的技術限制吧(complex 是 template,其 operator<< 必須在頭文件中定義,而這個定義又用到了 ostringstream,不得已包含了 iostream 的實現)。 若是程序要對 complex 作 IO,從效率和健壯性方面考慮,建議不要使用 iostream。 iostream 在使用方面的缺點 在簡單使用 iostream 的時候,它確實比 stdio 方便,可是深刻一點就會發現,兩者可說各擅勝場。下面談一談 iostream 在使用方面的缺點。 1. 格式化輸出很繁瑣 iostream 採用 manipulator 來格式化,若是我想按照 2010-04-03 的格式輸出前面定義的 Date class,那麼代碼要改爲: --- 02-02.cc 2011-07-16 16:40:05.000000000 +0800 +++ 04-01.cc 2011-07-16 17:10:27.000000000 +0800 @@ -1,4 +1,5 @@ #include <iostream> +#include <iomanip> class Date { @@ -10,7 +11,9 @@ void writeTo(std::ostream& os) const { - os << year_ << '-' << month_ << '-' << day_; + os << year_ << '-' + << std::setw(2) << std::setfill('0') << month_ << '-' + << std::setw(2) << std::setfill('0') << day_; } private: 假如用 stdio,會簡短得多,由於 printf 採用了一種表達能力較強的小語言來描述輸出格式。 --- 04-01.cc 2011-07-16 17:03:22.000000000 +0800 +++ 04-02.cc 2011-07-16 17:04:21.000000000 +0800 @@ -1,5 +1,5 @@ #include <iostream> -#include <iomanip> +#include <stdio.h> class Date { @@ -11,9 +11,9 @@ void writeTo(std::ostream& os) const { - os << year_ << '-' << month_ << '-' << day_; + char buf[32]; + snprintf(buf, sizeof buf, "%d-%02d-%02d" , year_, month_, day_); + os << buf; } private: 使用小語言來描述格式還帶來另一個好處:外部可配置。 2. 外部可配置性 比方說,我想用一個外部的配置文件來定義日期的格式。C stdio 很好辦,把格式字符串 "%d-%02d-%02d" 保存到配置裏就行。可是 iostream 呢?它的格式是寫死在代碼裏的,靈活性大打折扣。 再舉一個例子,程序的 message 的多語言化。 const char* name = "Shuo Chen"; int age = 29; printf("My name is %1$s, I am %2$d years old.\n", name, age); cout << "My name is " << name << ", I am " << age << " years old." << endl; 對於 stdio,要讓這段程序支持中文的話,把代碼中的"My name is %1$s, I am %2$d years old.\n", 替換爲 "我叫%1$s,今年%2$d歲。\n" 便可。也能夠把這段提示語作成資源文件,在運行時讀入。而對於 iostream,恐怕沒有這麼方便,由於代碼是支離破碎的。 C stdio 的格式化字符串體現了重要的「數據就是代碼」的思想,這種「數據」與「代碼」之間的相互轉換是程序靈活性的根源,遠比 OO 更爲靈活。 3. stream 的狀態 若是我想用 16 進制方式輸出一個整數 x,那麼能夠用 hex 操控符,可是這會改變 ostream 的狀態。好比說 int x = 8888; cout << hex << showbase << x << endl; // forgot to reset state cout << 123 << endl; 這這段代碼會把 123 也按照 16 進制方式輸出,這恐怕不是咱們想要的。 再舉一個例子,setprecision() 也會形成持續影響: double d = 123.45; printf("%8.3f\n", d); cout << d << endl; cout << setw(8) << fixed << setprecision(3) << d << endl; cout << d << endl; 輸出是: $ ./a.out 123.450 123.45 # default cout format 123.450 # our format 123.450 # side effects 可見代碼中的 setprecision() 影響了後續輸出的精度。注意 setw() 不會形成影響,它只對下一個輸出有效。 這說明,若是使用 manipulator 來控制格式,須要時刻當心防止影響了後續代碼。而使用 C stdio 就沒有這個問題,它是「上下文無關的」。 4. 知識的通用性 在 C 語言以外,有其餘不少語言也支持 printf() 風格的格式化,例如 Java、Perl、Ruby 等等 ( http://en.wikipedia.org/wiki/Printf#Programming_languages_with_printf)。學會 printf() 的格式化方法,這個知識還能夠用到其餘語言中。可是 C++ iostream 只此一家別無分店,反正都是格式化輸出,stdio 的投資回報率更高。 基於這點考慮,我認爲沒必要深究 iostream 的格式化方法,只須要用好它最基本的類型安全輸出便可。在真的須要格式化的場合,能夠考慮 snprintf() 打印到棧上緩衝,再用 ostream 輸出。 5. 線程安全與原子性 iostream 的另一個問題是線程安全性。stdio 的函數是線程安全的,並且 C 語言還提供了 flockfile(3)/funlockfile(3) 之類的函數來明確控制 FILE* 的加鎖與解鎖。 iostream 在線程安全方面沒有保證,就算單個 operator<< 是線程安全的,也不能保證原子性。由於 cout << a << b; 是兩次函數調用,至關於 cout.operator<<(a).operator<<(b)。兩次調用中間可能會被打斷進行上下文切換,形成輸出內容不連續,插入了其餘線程打印的字符。 而 fprintf(stdout, "%s %d", a, b); 是一次函數調用,並且是線程安全的,打印的內容不會受其餘線程影響。 所以,iostream 並不適合在多線程程序中作 logging。 iostream 的侷限 根據以上分析,咱們能夠概括 iostream 的侷限: 輸入方面,istream 不適合輸入帶格式的數據,由於「糾錯」能力不強,進一步的分析請見孟巖寫的《契約思想的一個反面案例》,孟巖說「複雜的設計必然帶來複雜的使用規則,而面對複雜的使用規則,用戶是能夠投票的,那就是你作你的,我不用!」可謂鞭辟入裏。若是要用 istream,我推薦的作法是用 getline() 讀入一行數據,而後用正則表達式來判斷內容正誤,並作分組,而後用 strtod/strtol 之類的函數作類型轉換。這樣彷佛更容易寫出健壯的程序。 輸出方面,ostream 的格式化輸出很是繁瑣,並且寫死在代碼裏,不如 stdio 的小語言那麼靈活通用。建議只用做簡單的無格式輸出。 log 方面,因爲 ostream 沒有辦法在多線程程序中保證一行輸出的完整性,建議不要直接用它來寫 log。若是是簡單的單線程程序,輸出數據量較少的狀況下能夠酌情使用。固然,產品代碼應該用成熟的 logging 庫,而不要用其它東西來湊合。 in-memory 格式化方面,因爲 ostringstream 會動態分配內存,它不適合性能要求較高的場合。 文件 IO 方面,若是用做文本文件的輸入或輸出,(i|o)fstream 有上述的缺點;若是用做二進制數據輸入輸出,那麼本身簡單封裝一個 File class 彷佛更好用,也沒必要爲用不到的功能付出代價(後文還有具體例子)。ifstream 的一個用處是在程序啓動時讀入簡單的文本配置文件。若是配置文件是其餘文本格式(XML 或 JSON),那麼用相應的庫來讀,也用不到 ifstream。 性能方面,iostream 沒有兌現「高效性」諾言。iostream 在某些場合比 stdio 快,在某些場合比 stdio 慢,對於性能要求較高的場合,咱們應該本身實現字符串轉換(見後文的代碼與測試)。iostream 性能方面的一個註腳:在線 ACM/ICPC 判題網站上,若是一個簡單的題目發生超時錯誤,那麼把其中 iostream 的輸入輸出換成 stdio,有時就能過關。 既然有這麼多侷限,iostream 在實際項目中的應用就大爲受限了,在這上面投入太多的精力實在不值得。說實話,我沒有見過哪一個 C++ 產品代碼使用 iostream 來做爲輸入輸出設施。 http://google-styleguide.googlecode.com/svn/trunk/cppguide.xml#Streams iostream 在設計方面的缺點 iostream 的設計有至關多的 WTFs,stackoverflow 有人吐槽說「If you had to judge by today's software engineering standards, would C++'s IOStreams still be considered well-designed?」 http://stackoverflow.com/questions/2753060/who-architected-designed-cs-iostreams-and-would-it-still-be-considered-well 。 面向對象的設計 iostream 是個面向對象的 IO 類庫,本節簡單介紹它的繼承體系。 對 iostream 略有了解的人會知道它用了多重繼承和虛擬繼承,簡單地畫個類圖以下,是典型的菱形繼承: 若是加深一點了解,會發現 iostream 如今是模板化的,同時支持窄字符和寬字符。下圖是如今的繼承體系,同時畫出了 fstreams 和 stringstreams。圖中方框的第二行是模板的具現化類型,也就是咱們代碼裏經常使用的具體類型(經過 typedef 定義)。 這個繼承體系糅合了面向對象與泛型編程,但惋惜它兩方面都不討好。 再進一步加深瞭解,發現還有一個平行的 streambuf 繼承體系,fstream 和 stringstream 的不一樣之處主要就在於它們使用了不一樣的 streambuf 具體類型。 再把這兩個繼承體系畫到一幅圖裏: 注意到 basic_ios 持有了 streambuf 的指針;而 fstreams 和 stringstreams 則分別包含 filebuf 和 stringbuf 的對象。看上去有點像 Bridge 模式。 看了這樣巴洛克的設計,有沒有人還打算在本身的項目中想經過繼承 iostream 來實現本身的 stream,以實現功能擴展麼? 面向對象方面的設計缺陷 本節咱們分析一下 iostream 的設計違反了哪些 OO 準則。 咱們知道,面向對象中的 public 繼承須要知足 Liskov 替換原則。(見《Effective C++ 第3版》條款32:確保你的 public 繼承模塑出 is-a 關係。《C++ 編程規範》條款 37:public 繼承意味可替換性。繼承非爲複用,乃爲被複用。) 在程序裏須要用到 ostream 的地方(例如 operator<< ),我傳入 ofstream 或 ostringstream 都應該能按預期工做,這就是 OO 繼承強調的「可替換性」,派生類的對象能夠替換基類對象,從而被 operator<< 複用。 iostream 的繼承體系屢次違反了 Liskov 原則,這些地方繼承的目的是爲了複用基類的代碼,下圖中我把違規的繼承關係用紅線標出。 在現有的繼承體系中,合理的有: ifstream is-aistream istringstream is-aistream ofstream is-aostream ostringstream is-aostream fstream is-aiostream stringstream is-a iostream 我認爲不怎麼合理的有: ios 繼承 ios_base,有沒有哪一種狀況下程序代碼期待 ios_base 對象,可是客戶能夠傳入一個 ios 對象替代之?若是沒有,這裏用 public 繼承是否是違反 OO 原則? istream 繼承 ios,有沒有哪一種狀況下程序代碼期待 ios 對象,可是客戶能夠傳入一個 istream 對象替代之?若是沒有,這裏用 public 繼承是否是違反 OO 原則? ostream 繼承 ios,有沒有哪一種狀況下程序代碼期待 ios 對象,可是客戶能夠傳入一個 ostream 對象替代之?若是沒有,這裏用 public 繼承是否是違反 OO 原則? iostream 多重繼承 istream 和 ostream。爲何 iostream 要同時繼承兩個 non-interface class?這是接口繼承仍是實現繼承?是否是能夠用組合(composition)來替代?(見《Effective C++ 第3版》條款38:經過組合模塑出 has-a 或「以某物實現」。《C++ 編程規範》條款 34:儘量以組合代替繼承。) 用組合替換繼承以後的體系: 注意到在新的設計中,只有真正的 is-a 關係採用了 public 繼承,其餘均以組合來代替,組合關係以紅線表示。新的設計沒有用的虛擬繼承或多重繼承。 其中 iostream 的新實現值得一提,代碼結構以下: class istream; class ostream; class iostream { public: istream& get_istream(); ostream& get_ostream(); virtual ~iostream(); }; 這樣一來,在須要 iostream 對象表現得像 istream 的地方,調用 get_istream() 函數返回一個 istream 的引用;在須要 iostream 對象表現得像 ostream 的地方,調用 get_ostream() 函數返回一個 ostream 的引用。功能不受影響,並且代碼更清晰。(雖然我很是懷疑 iostream 的真正價值,一個東西既可讀又可寫,說明是個 sophisticated IO 對象,爲何還用這麼厚的 OO 封裝?) 陽春的 locale iostream 的故事還不止這些,它還包含一套陽春的 locale/facet 實現,這套實踐中沒人用的東西進一步增長了 iostream 的複雜度,並且不可避免地影響其性能。Nathan Myers 正是始做俑者 http://www.cantrip.org/locale.html 。 ostream 自身定義的針對整數和浮點數的 operator<< 成員函數的函數體是: bool failed = use_facet<num_put>(getloc()).put( ostreambuf_iterator(*this), *this, fill(), val).failed(); 它會轉而調用 num_put::put(),後者會調用 num_put::do_put(),而 do_put() 是個虛函數,沒辦法 inline。iostream 在性能方面的不足恐怕部分來自於此。這個虛函數白白浪費了把 template 的實現放到頭文件應得的好處,編譯和運行速度都快不起來。 我沒有深刻挖掘其中的細節,感興趣的同窗能夠移步觀看 facet 的繼承體系: http://gcc.gnu.org/onlinedocs/libstdc++/libstdc++-html-USERS-4.4/a00431.html 據此分析,我不認爲以 iostream 爲基礎的上層程序庫(比方說那些克服 iostream 格式化方面的缺點的庫)有多大的實用價值。 臆造抽象 孟巖評價 「 iostream 最大的缺點是臆造抽象」,我很是贊同他老人家的觀點。 這個評價一樣適用於 Java 那一套疊牀架屋的 InputStream/OutputStream/Reader/Writer 繼承體系,.NET 也搞了這麼一套繁文縟節。 乍看之下,用 input stream 表示一個能夠「讀」的數據流,用 output stream 表示一個能夠「寫」的數據流,屏蔽底層細節,面向接口編程,「符合面向對象原則」,彷佛是一件美妙的事情。可是,真實的世界要殘酷得多。 IO 是個極度複雜的東西,就拿最多見的 memory stream、file stream、socket stream 來講,它們之間的差別極大: 是單向 IO 仍是雙向 IO。只讀或者只寫?仍是既可讀又可寫? 順序訪問仍是隨機訪問。可不能夠 seek?可不能夠退回 n 字節? 文本數據仍是二進制數據。格式有誤怎麼辦?如何編寫健壯的處理輸入的代碼? 有無緩衝。write 500 字節是否能保證徹底寫入?有沒有可能只寫入了 300 字節?餘下 200 字節怎麼辦? 是否阻塞。會不會返回 EWOULDBLOCK 錯誤? 有哪些出錯的狀況。這是最難的,memory stream 幾乎不可能出錯,file stream 和 socket stream 的出錯狀況徹底不一樣。socket stream 可能遇到對方斷開鏈接,file stream 可能遇到超出磁盤配額。 根據以上列舉的初步分析,我不認爲有辦法設計一個公共的基類把各方面的狀況都考慮周全。各類 IO 設施之間共性過小,差別太大,例外太多。若是硬要用面向對象來建模,基類要麼太瘦(只放共性,這個基類包含的 interface functions 沒多大用),要麼太肥(把各類 IO 設施的特性都包含進來,這個基類包含的 interface functions 不少,可是不是每個都能調用)。 C 語言對此的解決辦法是用一個 int 表示 IO 對象(file 或 PIPE 或 socket),而後配以 read()/write()/lseek()/fcntl() 等一系列全局函數,程序員本身搭配組合。這個作法我認爲比面向對象的方案要簡潔高效。 iostream 在性能方面沒有比 stdio 高多少,在健壯性方面多半不如 stdio,在靈活性方面受制於自己的複雜設計而難以讓使用者自行擴展。目前看起來只適合一些簡單的要求不高的應用,可是又不得不爲它的複雜設計付出運行時代價,總之其定位有點不上不下。 在實際的項目中,咱們能夠提煉出一些簡單高效的 strip-down 版本,在得到便利性的同時避免付出沒必要要的代價。 一個 300 行的 memory buffer output stream 我認爲以 operator<< 來輸出數據很是適合 logging,所以寫了一個簡單的 LogStream。代碼不到 300行,徹底獨立於 iostream。 接口 https://github.com/chenshuo/recipes/blob/master/logging/LogStream.h 實現 https://github.com/chenshuo/recipes/blob/master/logging/LogStream.cc 單元測試 https://github.com/chenshuo/recipes/blob/master/logging/LogStream_test.cc 性能測試 https://github.com/chenshuo/recipes/blob/master/logging/LogStream_bench.cc 這個 LogStream 作到了類型安全和類型可擴展。它不支持定製格式化、不支持 locale/facet、沒有繼承、buffer 也沒有繼承與虛函數、沒有動態分配內存、buffer 大小固定。簡單地說,適合 logging 以及簡單的字符串轉換。 LogStream 的接口定義是 class LogStream : boost::noncopyable { typedef LogStream self; public: typedef detail::FixedBuffer Buffer; LogStream(); self& operator<<(bool); self& operator<<(short); self& operator<<(unsigned short); self& operator<<(int); self& operator<<(unsigned int); self& operator<<(long); self& operator<<(unsigned long); self& operator<<(long long); self& operator<<(unsigned long long); self& operator<<(const void*); self& operator<<(float); self& operator<<(double); // self& operator<<(long double); self& operator<<(char); // self& operator<<(signed char); // self& operator<<(unsigned char); self& operator<<(const char*); self& operator<<(const string&); const Buffer& buffer() const { return buffer_; } void resetBuffer() { buffer_.reset(); } private: Buffer buffer_; }; LogStream 自己不是線程安全的,它不適合作全局對象。正確的使用方式是每條 log 消息構造一個 LogStream,用完就扔。LogStream 的成本極低,這麼作不會有什麼性能損失。 目前這個 logging 庫還在開發之中,只完成了 LogStream 這一部分。未來可能改用動態分配的 buffer,這樣方便在線程之間傳遞數據。 整數到字符串的高效轉換 muduo::LogStream 的整數轉換是本身寫的,用的是 Matthew Wilson 的算法,見 http://blog.csdn.net/solstice/article/details/5139302 。這個算法比 stdio 和 iostream 都要快。 浮點數到字符串的高效轉換 目前 muduo::LogStream 的浮點數格式化採用的是 snprintf() 因此從性能上與 stdio 持平,比 ostream 快一些。 浮點數到字符串的轉換是個複雜的話題,這個領域 20 年以來沒有什麼進展(目前的實現大都基於 David M. Gay 在 1990 年的工做《Correctly Rounded Binary-Decimal and Decimal-Binary Conversions》,代碼 http://netlib.org/fp/),直到 2010 年纔有突破。 Florian Loitsch 發明了新的更快的算法 Grisu3,他的論文《Printing floating-point numbers quickly and accurately with integers》發表在 PLDI 2010,代碼見 Google V8 引擎,還有這裏 http://code.google.com/p/double-conversion/ 。有興趣的同窗能夠閱讀這篇博客 http://www.serpentine.com/blog/2011/06/29/here-be-dragons-advances-in-problems-you-didnt-even-know-you-had/ 。 未來 muduo::LogStream 可能會改用 Grisu3 算法實現浮點數轉換。 性能對比 因爲 muduo::LogStream 拋掉了不少負擔,能夠預見它的性能好於 ostringstream 和 stdio。我作了一個簡單的性能測試,結果以下。 從上表看出,ostreamstream 有時候比 snprintf 快,有時候比它慢,muduo::LogStream 比它們兩個都快得多(double 類型除外)。 泛型編程 其餘程序庫如何使用 LogStream 做爲輸出呢?辦法很簡單,用模板。 前面咱們定義了 Date class 針對 std::ostream 的 operator<<,只要稍做修改就能同時適用於 std::ostream 和 LogStream。並且 Date 的頭文件再也不須要 include <ostream>,下降了耦合。 class Date { public: Date(int year, int month, int day) : year_(year), month_(month), day_(day) { } - void writeTo(std::ostream& os) const + template<typename OStream> + void writeTo(OStream& os) const { char buf[32]; snprintf(buf, sizeof buf, "%d-%02d-%02d", year_, month_, day_); os << buf; } private: int year_, month_, day_; }; -std::ostream& operator<<(std::ostream& os, const Date& date) +template<typename OStream> +OStream& operator<<(OStream& os, const Date& date) { date.writeTo(os); return os; } 現實的 C++ 程序如何作文件 IO 舉兩個例子, Kyoto Cabinet 和 Google leveldb。 Google leveldb Google leveldb 是一個高效的持久化 key-value db。 它定義了三個精簡的 interface: SequentialFile http://code.google.com/p/leveldb/source/browse/trunk/include/leveldb/env.h#154 RandomAccessFile http://code.google.com/p/leveldb/source/browse/trunk/include/leveldb/env.h#178 WritableFile http://code.google.com/p/leveldb/source/browse/trunk/include/leveldb/env.h#197 接口函數以下 struct Slice { const char* data_; size_t size_; }; // A file abstraction for reading sequentially through a file class SequentialFile { public: SequentialFile() { } virtual ~SequentialFile(); virtual Status Read(size_t n, Slice* result, char* scratch) = 0; virtual Status Skip(uint64_t n) = 0; }; // A file abstraction for randomly reading the contents of a file. class RandomAccessFile { public: RandomAccessFile() { } virtual ~RandomAccessFile(); virtual Status Read(uint64_t offset, size_t n, Slice* result, char* scratch) const = 0; }; // A file abstraction for sequential writing. The implementation // must provide buffering since callers may append small fragments // at a time to the file. class WritableFile { public: WritableFile() { } virtual ~WritableFile(); virtual Status Append(const Slice& data) = 0; virtual Status Close() = 0; virtual Status Flush() = 0; virtual Status Sync() = 0; }; leveldb 明確區分 input 和 output,進一步它又把 input 分爲 sequential 和 random access,而後提煉出了三個簡單的接口,每一個接口只有屈指可數的幾個函數。這幾個接口在各個平臺下的實現也很是簡單明瞭( http://code.google.com/p/leveldb/source/browse/trunk/util/env_posix.cc#35 http://code.google.com/p/leveldb/source/browse/trunk/util/env_chromium.cc#176),一看就懂。 注意這三個接口使用了虛函數,我認爲這是正當的,由於一次 IO 每每伴隨着 context switch,虛函數的開銷比起 context switch 來能夠忽略不計。相反,iostream 每次 operator<<() 就調用虛函數,我認爲不太明智。 Kyoto Cabinet Kyoto Cabinet 也是一個 key-value db,是前幾年流行的 Tokyo Cabinet 的升級版。它採用了與 leveldb 不一樣的文件抽象。 KC 定義了一個 File class,同時包含了讀寫操做,這是個 fat interface。 http://fallabs.com/kyotocabinet/api/classkyotocabinet_1_1File.html 在具體實現方面,它沒有使用虛函數,而是採用 #ifdef 來區分不一樣的平臺(見 http://code.google.com/p/read-taobao-code/source/browse/trunk/tair/src/storage/kdb/kyotocabinet/kcfile.cc),等於把兩份獨立的代碼寫到了同一個文件裏邊。 相比之下,Google leveldb 的作法更高明一些。 小結 在 C++ 項目裏邊本身寫個 File class,把項目用到的文件 IO 功能簡單封裝一下(以 RAII 手法封裝 FILE* 或者 file descriptor 均可以,視狀況而定),一般就能知足須要。記得把拷貝構造和賦值操做符禁用,在析構函數裏釋放資源,避免泄露內部的 handle,這樣就能自動避免不少 C 語言文件操做的常見錯誤。 若是要用 stream 方式作 logging,能夠拋開繁重的 iostream 本身寫一個簡單的 LogStream,重載幾個 operator<<,用起來同樣方便;並且能夠用 stack buffer,輕鬆作到線程安全。 |