青青草原综合久久大伊人导航_色综合久久天天综合_日日噜噜夜夜狠狠久久丁香五月_热久久这里只有精品

陳碩的Blog

C++ 工程實(shí)踐(7):iostream 的用途與局限

陳碩 (giantchen_AT_gmail)

http://blog.csdn.net/Solstice  http://weibo.com/giantchen

陳碩關(guān)于 C++ 工程實(shí)踐的系列文章: http://blog.csdn.net/Solstice/category/802325.aspx

陳碩博客文章合集下載: http://blog.csdn.net/Solstice/archive/2011/02/24/6206154.aspx

本作品采用“Creative Commons 署名-非商業(yè)性使用-禁止演繹 3.0 Unported 許可協(xié)議(cc by-nc-nd)”進(jìn)行許可。http://creativecommons.org/licenses/by-nc-nd/3.0/

本文主要考慮 x86 Linux 平臺(tái),不考慮跨平臺(tái)的可移植性,也不考慮國(guó)際化(i18n),但是要考慮 32-bit 和 64-bit 的兼容性。本文以 stdio 指代 C 語言的 scanf/printf 系列格式化輸入輸出函數(shù)。本文注意區(qū)分“編程初學(xué)者”和“C++初學(xué)者”,二者含義不同。

摘要:C++ iostream 的主要作用是讓初學(xué)者有一個(gè)方便的命令行輸入輸出試驗(yàn)環(huán)境,在真實(shí)的項(xiàng)目中很少用到 iostream,因此不必把精力花在深究 iostream 的格式化與 manipulator。iostream 的設(shè)計(jì)初衷是提供一個(gè)可擴(kuò)展的類型安全的 IO 機(jī)制,但是后來莫名其妙地加入了 locale 和 facet 等累贅。其整個(gè)設(shè)計(jì)復(fù)雜不堪,多重+虛擬繼承的結(jié)構(gòu)也很巴洛克,性能方面幾無亮點(diǎn)。iostream 在實(shí)際項(xiàng)目中的用處非常有限,為此投入過多學(xué)習(xí)精力實(shí)在不值。

stdio 格式化輸入輸出的缺點(diǎn)

1. 對(duì)編程初學(xué)者不友好

看看下面這段簡(jiǎn)單的輸入輸出代碼。

#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。
  • 輸入的參數(shù)不統(tǒng)一。對(duì)于 i、s、f、d 等變量,在傳入 scanf() 的時(shí)候要取地址(&),而對(duì)于 name,則不用取地址。

讀者可以試一試如何用幾句話向剛開始學(xué)編程的初學(xué)者解釋上面兩條背后原因(涉及到傳遞函數(shù)不定參數(shù)時(shí)的類型轉(zhuǎn)換,函數(shù)調(diào)用棧的內(nèi)存布局,指針的意義,字符數(shù)組退化為字符指針等等),如果一開始解釋不清,只好告訴學(xué)生“這是規(guī)定”。

  • 緩沖區(qū)溢出的危險(xiǎn)。上面的例子在讀入 name 的時(shí)候沒有指定大小,這是用 C 語言編程的安全漏洞的主要來源。應(yīng)該在一開始就強(qiáng)調(diào)正確的做法,避免養(yǎng)成錯(cuò)誤的習(xí)慣。正確而安全的做法如 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);
}

這個(gè)動(dòng)態(tài)構(gòu)造格式化字符串的做法恐怕更難向初學(xué)者解釋。

2. 安全性(security)

C 語言的安全性問題近十幾年來引起了廣泛的注意,C99 增加了 snprintf() 等能夠指定輸出緩沖區(qū)大小的函數(shù),輸出方面的安全性問題已經(jīng)得到解決;輸入方面似乎沒有太大進(jìn)展,還要靠程序員自己動(dòng)手。

考慮一個(gè)簡(jiǎn)單的編程任務(wù):從文件或標(biāo)準(zhǔn)輸入讀入一行字符串,行的長(zhǎng)度不確定。我發(fā)現(xiàn)沒有哪個(gè) C 語言標(biāo)準(zhǔn)庫函數(shù)能完成這個(gè)任務(wù),除非 roll your own。

首先,gets() 是錯(cuò)誤的,因?yàn)椴荒苤付ň彌_區(qū)的長(zhǎng)度。

其次,fgets() 也有問題。它能指定緩沖區(qū)的長(zhǎng)度,所以是安全的。但是程序必須預(yù)設(shè)一個(gè)長(zhǎng)度的最大值,這不滿足題目要求“行的長(zhǎng)度不確定”。另外,程序無法判斷 fgets() 到底讀了多少個(gè)字節(jié)。為什么?考慮一個(gè)文件的內(nèi)容是 9 個(gè)字節(jié)的字符串 "Chen\000Shuo",注意中間出現(xiàn)了 '\0' 字符,如果用 fgets() 來讀取,客戶端如何知道 "\000Shuo" 也是輸入的一部分?畢竟 strlen() 只返回 4,而且整個(gè)字符串里沒有 '\n' 字符。

最后,可以用 glibc 定義的 getline(3) 函數(shù)來讀取不定長(zhǎng)的“行”。這個(gè)函數(shù)能正確處理各種情況,不過它返回的是 malloc() 分配的內(nèi)存,要求調(diào)用端自己 free()。

3. 類型安全(type-safe)

如果 printf() 的整數(shù)參數(shù)類型是 int、long 等標(biāo)準(zhǔn)類型, 那么 printf() 的格式化字符串很容易寫。但是如果參數(shù)類型是 typedef 的類型呢?

如果你想在程序中用 printf 來打印日志,你能一眼看出下面這些類型該用 "%d" "%ld" "%lld" 中的哪一個(gè)來輸出?你的選擇是否同時(shí)兼容 32-bit 和 64-bit 平臺(tái)?

  • clock_t。這是 clock(3) 的返回類型
  • dev_t。這是 mknod(3) 的參數(shù)類型
  • in_addr_t、in_port_t。這是 struct sockaddr_in 的成員類型
  • nfds_t。這是 poll(2) 的參數(shù)類型
  • off_t。這是 lseek(2) 的參數(shù)類型,麻煩的是,這個(gè)類型與宏定義 _FILE_OFFSET_BITS 有關(guān)。
  • pid_t、uid_t、gid_t。這是 getpid(2) getuid(2) getgid(2) 的返回類型
  • ptrdiff_t。printf() 專門定義了 "t" 前綴來支持這一類型(即使用 "%td" 來打印)。
  • size_t、ssize_t。這兩個(gè)類型到處都在用。printf() 為此專門定義了 "z" 前綴來支持這兩個(gè)類型(即使用 "%zu" 或 "%zd" 來打印)。
  • socklen_t。這是 bind(2) 和 connect(2) 的參數(shù)類型
  • time_t。這是 time(2) 的返回類型,也是 gettimeofday(2) 和 clock_gettime(2) 的輸出結(jié)構(gòu)體的成員類型

如果在 C 程序里要正確打印以上類型的整數(shù),恐怕要費(fèi)一番腦筋。《The Linux Programming Interface》的作者建議(3.6.2節(jié))先統(tǒng)一轉(zhuǎn)換為 long 類型再用 "%ld" 來打印;對(duì)于某些類型仍然需要特殊處理,比如 off_t 的類型可能是 long long。

還有,int64_t 在 32-bit 和 64-bit 平臺(tái)上是不同的類型,為此,如果程序要打印 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++ 編碼規(guī)范也提到了 64-bit 兼容性: http://google-styleguide.googlecode.com/svn/trunk/cppguide.xml#64-bit_Portability

這些問題在 C++ 里都不存在,在這方面 iostream 是個(gè)進(jìn)步。

C stdio 在類型安全方面原本還有一個(gè)缺點(diǎn),即格式化字符串與參數(shù)類型不匹配會(huì)造成難以發(fā)現(xiàn)的 bug,不過現(xiàn)在的編譯器已經(jīng)能夠檢測(cè)很多這種錯(cuò)誤:

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. 不可擴(kuò)展?

C stdio 的另外一個(gè)缺點(diǎn)是無法支持自定義的類型,比如我寫了一個(gè) Date class,我無法像打印 int 那樣用 printf 來直接打印 Date 對(duì)象。

struct Date
{
  int year, month, day;
};

Date date;
printf("%D", &date);  // WRONG

Glibc 放寬了這個(gè)限制,允許用戶調(diào)用 register_printf_function(3) 注冊(cè)自己的類型,當(dāng)然,前提是與現(xiàn)有的格式字符不沖突(這其實(shí)大大限制了這個(gè)功能的用處,現(xiàn)實(shí)中也幾乎沒有人真的去用它)。http://www.gnu.org/s/hello/manual/libc/Printf-Extension-Example.html  http://en.wikipedia.org/wiki/Printf#Custom_format_placeholders

5. 性能

C stdio 的性能方面有兩個(gè)弱點(diǎn)。

  1. 使用一種 little language (現(xiàn)在流行叫 DSL)來配置格式。固然有利于緊湊性和靈活性,但損失了一點(diǎn)點(diǎn)效率。每次打印一個(gè)整數(shù)都要先解析 "%d" 字符串,大多數(shù)情況下不是問題,某些場(chǎng)合需要自己寫整數(shù)到字符串的轉(zhuǎn)換。
  2. C locale 的負(fù)擔(dān)。locale 指的是不同語種對(duì)“什么是空白”、“什么是字母”,“什么是小數(shù)點(diǎn)”有不同的定義(德語里邊小數(shù)點(diǎn)是逗號(hào),不是句點(diǎn))。C 語言的 printf()、scanf()、isspace()、isalpha()、ispunct()、strtod() 等等函數(shù)都和 locale 有關(guān),而且可以在運(yùn)行時(shí)動(dòng)態(tài)更改。就算是程序只使用默認(rèn)的 "C" locale,任然要為這個(gè)靈活性付出代價(jià)。

iostream 的設(shè)計(jì)初衷

iostream 的設(shè)計(jì)初衷包括克服 C stdio 的缺點(diǎn),提供一個(gè)高效的可擴(kuò)展的類型安全的 IO 機(jī)制。“可擴(kuò)展”有兩層意思,一是可以擴(kuò)展到用戶自定義類型,而是通過繼承 iostream 來定義自己的 stream,本文把前一種稱為“類型可擴(kuò)展”后一種稱為“功能可擴(kuò)展”。

“類型可擴(kuò)展”和“類型安全”都是通過函數(shù)重載來實(shí)現(xiàn)的。

iostream 對(duì)初學(xué)者很友好,用 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 類型沒有區(qū)別。這主要得力于 C++ 可以定義 non-member functions/operators。

#include <ostream>  // 是不是太重量級(jí)了?

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 憑借這兩點(diǎn)(類型安全和類型可擴(kuò)展),基本克服了 stdio 在使用上的不便與不安全。如果 iostream 止步于此,那它將是一個(gè)非常便利的庫,可惜它前進(jìn)了另外一步。

iostream 與標(biāo)準(zhǔn)庫其他組件的交互

不同于標(biāo)準(zhǔn)庫其他 class 的“值語意”,iostream 是“對(duì)象語意”,即 iostream 是 non-copyable。這是正確的,因?yàn)槿绻?fstream 代表一個(gè)文件的話,拷貝一個(gè) fstream 對(duì)象意味著什么呢?表示打開了兩個(gè)文件嗎?如果銷毀一個(gè) fstream 對(duì)象,它會(huì)關(guān)閉文件句柄,那么另一個(gè) fstream copy 對(duì)象會(huì)因此受影響嗎?

C++ 同時(shí)支持“數(shù)據(jù)抽象”和“面向?qū)ο缶幊?#8221;,其實(shí)主要就是“值語意”與“對(duì)象語意”的區(qū)別,我發(fā)現(xiàn)不是每個(gè)人都清楚這一點(diǎn),這里多說幾句。標(biāo)準(zhǔn)庫里的 complex<> 、pair<>、vector<>、 string 等等都是值語意,拷貝之后就與原對(duì)象脫離關(guān)系,就跟拷貝一個(gè) int 一樣。而我們自己寫的 Employee class、TcpConnection class 通常是對(duì)象語意,拷貝一個(gè) Employee 對(duì)象是沒有意義的,一個(gè)雇員不會(huì)變成兩個(gè)雇員,他也不會(huì)領(lǐng)兩份薪水。拷貝 TcpConnection 對(duì)象也沒有意義,系統(tǒng)里邊只有一個(gè) TCP 連接,拷貝 TcpConnection  對(duì)象不會(huì)讓我們擁有兩個(gè)連接。因此如果在 C++ 里做面向?qū)ο缶幊蹋瑢懙?class 通常應(yīng)該禁用 copy constructor 和 assignment operator,比如可以繼承 boost::noncopyable。對(duì)象語意的類型不能直接作為標(biāo)準(zhǔn)容器庫的成員。另一方面,如果要寫一個(gè)圖形程序,其中用到三維空間的向量,那么我們可以寫 Vector3D class,它應(yīng)該是值語意的,允許拷貝,并且可以用作標(biāo)準(zhǔn)容器庫的成員,例如 vector<Vector3D> 表示一條三維的折線。

C stdio 的另外一個(gè)缺點(diǎn)是 FILE* 可以隨意拷貝,但是只要關(guān)閉其中一個(gè) copy,其他 copies 也都失效了,跟空懸指針一般。這其實(shí)不光是 C stdio 的缺點(diǎn),整個(gè) C 語言對(duì)待資源(malloc 得到的內(nèi)存,open() 打開的文件,socket() 打開的連接)都是這樣,用整數(shù)或指針來代表(即“句柄”)。而整數(shù)和指針類型的“句柄”是可以隨意拷貝的,很容易就造成重復(fù)釋放、遺漏釋放、使用已經(jīng)釋放的資源等等常見錯(cuò)誤。這是因?yàn)?C 語言錯(cuò)誤地讓“對(duì)象語言”的東西變成了值語意。

iostream 禁止拷貝,利用對(duì)象的生命期來明確管理資源(如文件),很自然地就避免了 C 語言易犯的錯(cuò)誤。這就是 RAII,一種重要且獨(dú)特的 C++ 編程手法。

std::string

iostream 可以與 string 配合得很好。但是有一個(gè)問題:誰依賴誰?

std::string 的 operator << 和 operator >> 是如何聲明的?"string" 頭文件在聲明這兩個(gè) operators 的時(shí)候要不要 include "iostream" ?

iostream 和 string 都可以單獨(dú) include 來使用,顯然 iostream 頭文件里不會(huì)定義 string 的 << 和 >> 操作。但是,如果"string"要include "iostream",豈不是讓 string 的用戶被迫也用了 iostream?編譯 iostream 頭文件可是相當(dāng)?shù)穆。ㄒ驗(yàn)?iostream 是 template,其實(shí)現(xiàn)代碼都放到了頭文件中)。

標(biāo)準(zhǔn)庫的解決辦法是定義 iosfwd 頭文件,其中包含 istream 和 ostream 等的前向聲明 (forward declarations),這樣 "string" 頭文件在定義輸入輸出操作符時(shí)就可以不必包含 "iostream",只需要包含簡(jiǎn)短得多的 "iosfwd"。我們自己寫程序也可借此學(xué)習(xí)如何支持可選的功能。

值得注意的是,istream::getline() 成員函數(shù)的參數(shù)類型是 char*,因?yàn)?"istream" 沒有包含 "string",而我們常用的 std::getline() 函數(shù)是個(gè) non-member function,定義在 "string" 里邊。

std::complex

標(biāo)準(zhǔn)庫的復(fù)數(shù)類 complex 的情況比較復(fù)雜。使用 complex 會(huì)自動(dòng)包含 sstream,后者會(huì)包含 istream 和 ostream,這是個(gè)不小的負(fù)擔(dān)。問題是,為什么?

它的 operator >> 操作比 string 復(fù)雜得多,如何應(yīng)對(duì)格式不正確的情況?輸入字符串不會(huì)遇到格式不正確,但是輸入一個(gè)復(fù)數(shù)可能遇到各種問題,比如數(shù)字的格式不對(duì)等。我懷疑有誰會(huì)真的在產(chǎn)品項(xiàng)目里用 operator >> 來讀入字符方式表示的復(fù)數(shù),這樣的代碼的健壯性如何保證。基于同樣的理由,我認(rèn)為產(chǎn)品代碼中應(yīng)該避免用 istream 來讀取帶格式的內(nèi)容,后面也不再談 istream 的缺點(diǎn),它已經(jīng)被秒殺。

它的 operator << 也很奇怪,它不是直接使用參數(shù) ostream& os 對(duì)象來輸出,而是先構(gòu)造 ostringstream,輸出到該 string stream,再把結(jié)果字符串輸出到 ostream。簡(jiǎn)化后的代碼如下:

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 會(huì)用到動(dòng)態(tài)分配內(nèi)存,也就是說,每輸出一個(gè) complex 對(duì)象就會(huì)分配釋放一次內(nèi)存,效率堪憂。

根據(jù)以上分析,我認(rèn)為 iostream 和 complex 配合得不好,但是它們耦合得更緊密(與 string/iostream 相比),這可能是個(gè)不得已的技術(shù)限制吧(complex 是 template,其 operator<< 必須在頭文件中定義,而這個(gè)定義又用到了 ostringstream,不得已包含了 iostream 的實(shí)現(xiàn))。

如果程序要對(duì) complex 做 IO,從效率和健壯性方面考慮,建議不要使用 iostream。

iostream 在使用方面的缺點(diǎn)

在簡(jiǎn)單使用 iostream 的時(shí)候,它確實(shí)比 stdio 方便,但是深入一點(diǎn)就會(huì)發(fā)現(xiàn),二者可說各擅勝場(chǎng)。下面談一談 iostream 在使用方面的缺點(diǎn)。

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,會(huì)簡(jiǎn)短得多,因?yàn)?printf 采用了一種表達(dá)能力較強(qiáng)的小語言來描述輸出格式。

--- 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:

使用小語言來描述格式還帶來另外一個(gè)好處:外部可配置。

2. 外部可配置性

比方說,我想用一個(gè)外部的配置文件來定義日期的格式。C stdio 很好辦,把格式字符串 "%d-%02d-%02d" 保存到配置里就行。但是 iostream 呢?它的格式是寫死在代碼里的,靈活性大打折扣。

再舉一個(gè)例子,程序的 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;
對(duì)于 stdio,要讓這段程序支持中文的話,把代碼中的"My name is %1$s, I am %2$d years old.\n",

替換為 "我叫%1$s,今年%2$d歲。\n" 即可。也可以把這段提示語做成資源文件,在運(yùn)行時(shí)讀入。而對(duì)于 iostream,恐怕沒有這么方便,因?yàn)榇a是支離破碎的。

C stdio 的格式化字符串體現(xiàn)了重要的“數(shù)據(jù)就是代碼”的思想,這種“數(shù)據(jù)”與“代碼”之間的相互轉(zhuǎn)換是程序靈活性的根源,遠(yuǎn)比 OO 更為靈活。

3. stream 的狀態(tài)

如果我想用 16 進(jìn)制方式輸出一個(gè)整數(shù) x,那么可以用 hex 操控符,但是這會(huì)改變 ostream 的狀態(tài)。比如說

  int x = 8888;
  cout << hex << showbase << x << endl;  // forgot to reset state
  cout << 123 << endl;

這這段代碼會(huì)把 123 也按照 16 進(jìn)制方式輸出,這恐怕不是我們想要的。

再舉一個(gè)例子,setprecision() 也會(huì)造成持續(xù)影響:

  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() 影響了后續(xù)輸出的精度。注意 setw() 不會(huì)造成影響,它只對(duì)下一個(gè)輸出有效。

這說明,如果使用 manipulator 來控制格式,需要時(shí)刻小心防止影響了后續(xù)代碼。而使用 C stdio 就沒有這個(gè)問題,它是“上下文無關(guān)的”。

4. 知識(shí)的通用性

在 C 語言之外,有其他很多語言也支持 printf() 風(fēng)格的格式化,例如 Java、Perl、Ruby 等等 (http://en.wikipedia.org/wiki/Printf#Programming_languages_with_printf)。學(xué)會(huì) printf() 的格式化方法,這個(gè)知識(shí)還可以用到其他語言中。但是 C++ iostream 只此一家別無分店,反正都是格式化輸出,stdio 的投資回報(bào)率更高。

基于這點(diǎn)考慮,我認(rèn)為不必深究 iostream 的格式化方法,只需要用好它最基本的類型安全輸出即可。在真的需要格式化的場(chǎng)合,可以考慮 snprintf() 打印到棧上緩沖,再用 ostream 輸出。

5. 線程安全與原子性

iostream 的另外一個(gè)問題是線程安全性。stdio 的函數(shù)是線程安全的,而且 C 語言還提供了 flockfile(3)/funlockfile(3) 之類的函數(shù)來明確控制 FILE* 的加鎖與解鎖。

iostream 在線程安全方面沒有保證,就算單個(gè) operator<< 是線程安全的,也不能保證原子性。因?yàn)?cout << a << b; 是兩次函數(shù)調(diào)用,相當(dāng)于 cout.operator<<(a).operator<<(b)。兩次調(diào)用中間可能會(huì)被打斷進(jìn)行上下文切換,造成輸出內(nèi)容不連續(xù),插入了其他線程打印的字符。

而 fprintf(stdout, "%s %d", a, b); 是一次函數(shù)調(diào)用,而且是線程安全的,打印的內(nèi)容不會(huì)受其他線程影響。

因此,iostream 并不適合在多線程程序中做 logging。

iostream 的局限

根據(jù)以上分析,我們可以歸納 iostream 的局限:

  • 輸入方面,istream 不適合輸入帶格式的數(shù)據(jù),因?yàn)?#8220;糾錯(cuò)”能力不強(qiáng),進(jìn)一步的分析請(qǐng)見孟巖寫的《契約思想的一個(gè)反面案例》,孟巖說“復(fù)雜的設(shè)計(jì)必然帶來復(fù)雜的使用規(guī)則,而面對(duì)復(fù)雜的使用規(guī)則,用戶是可以投票的,那就是你做你的,我不用!”可謂鞭辟入里。如果要用 istream,我推薦的做法是用 getline() 讀入一行數(shù)據(jù),然后用正則表達(dá)式來判斷內(nèi)容正誤,并做分組,然后用 strtod/strtol 之類的函數(shù)做類型轉(zhuǎn)換。這樣似乎更容易寫出健壯的程序。
  • 輸出方面,ostream 的格式化輸出非常繁瑣,而且寫死在代碼里,不如 stdio 的小語言那么靈活通用。建議只用作簡(jiǎn)單的無格式輸出。
  • log 方面,由于 ostream 沒有辦法在多線程程序中保證一行輸出的完整性,建議不要直接用它來寫 log。如果是簡(jiǎn)單的單線程程序,輸出數(shù)據(jù)量較少的情況下可以酌情使用。當(dāng)然,產(chǎn)品代碼應(yīng)該用成熟的 logging 庫,而不要用其它東西來湊合。
  • in-memory 格式化方面,由于 ostringstream 會(huì)動(dòng)態(tài)分配內(nèi)存,它不適合性能要求較高的場(chǎng)合。
  • 文件 IO 方面,如果用作文本文件的輸入輸出,(i|o)fstream 有上述的缺點(diǎn);如果用作二進(jìn)制數(shù)據(jù)輸入輸出,那么自己簡(jiǎn)單封裝一個(gè) File class 似乎更好用,也不必為用不到的功能付出代價(jià)(后文還有具體例子)。ifstream 的一個(gè)用處是在程序啟動(dòng)時(shí)讀入簡(jiǎn)單的文本配置文件。如果配置文件是其他文本格式(XML 或 JSON),那么用相應(yīng)的庫來讀,也用不到 ifstream。
  • 性能方面,iostream 沒有兌現(xiàn)“高效性”諾言。iostream 在某些場(chǎng)合比 stdio 快,在某些場(chǎng)合比 stdio 慢,對(duì)于性能要求較高的場(chǎng)合,我們應(yīng)該自己實(shí)現(xiàn)字符串轉(zhuǎn)換(見后文的代碼與測(cè)試)。iostream 性能方面的一個(gè)注腳:在線 ACM/ICPC 判題網(wǎng)站上,如果一個(gè)簡(jiǎn)單的題目發(fā)生超時(shí)錯(cuò)誤,那么把其中 iostream 的輸入輸出換成 stdio,有時(shí)就能過關(guān)。

既然有這么多局限,iostream 在實(shí)際項(xiàng)目中的應(yīng)用就大為受限了,在這上面投入太多的精力實(shí)在不值得。說實(shí)話,我沒有見過哪個(gè) C++ 產(chǎn)品代碼使用 iostream 來作為輸入輸出設(shè)施。 http://google-styleguide.googlecode.com/svn/trunk/cppguide.xml#Streams 

iostream 在設(shè)計(jì)方面的缺點(diǎn)

iostream 的設(shè)計(jì)有相當(dāng)多的 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

面向?qū)ο蟮脑O(shè)計(jì)

iostream 是個(gè)面向?qū)ο蟮?IO 類庫,本節(jié)簡(jiǎn)單介紹它的繼承體系。

對(duì) iostream 略有了解的人會(huì)知道它用了多重繼承和虛擬繼承,簡(jiǎn)單地畫個(gè)類圖如下,是典型的菱形繼承:

simple

如果加深一點(diǎn)了解,會(huì)發(fā)現(xiàn) iostream 現(xiàn)在是模板化的,同時(shí)支持窄字符和寬字符。下圖是現(xiàn)在的繼承體系,同時(shí)畫出了 fstreams 和 stringstreams。圖中方框的第二行是模板的具現(xiàn)化類型,也就是我們代碼里常用的具體類型(通過 typedef 定義)。

 

ios

這個(gè)繼承體系糅合了面向?qū)ο笈c泛型編程,但可惜它兩方面都不討好。

再進(jìn)一步加深了解,發(fā)現(xiàn)還有一個(gè)平行的 streambuf 繼承體系,fstream 和 stringstream 的不同之處主要就在于它們使用了不同的 streambuf 具體類型。

 

buf

再把這兩個(gè)繼承體系畫到一幅圖里:

full

注意到 basic_ios 持有了 streambuf 的指針;而 fstreams 和 stringstreams 則分別包含 filebuf 和 stringbuf 的對(duì)象。看上去有點(diǎn)像 Bridge 模式。

 

看了這樣巴洛克的設(shè)計(jì),有沒有人還打算在自己的項(xiàng)目中想通過繼承 iostream 來實(shí)現(xiàn)自己的 stream,以實(shí)現(xiàn)功能擴(kuò)展么?

面向?qū)ο蠓矫娴脑O(shè)計(jì)缺陷

本節(jié)我們分析一下 iostream 的設(shè)計(jì)違反了哪些 OO 準(zhǔn)則。

我們知道,面向?qū)ο笾械?public 繼承需要滿足 Liskov 替換原則。(見《Effective C++ 第3版》條款32:確保你的 public 繼承模塑出 is-a 關(guān)系。《C++ 編程規(guī)范》條款 37:public 繼承意味可替換性。繼承非為復(fù)用,乃為被復(fù)用。)

在程序里需要用到 ostream 的地方(例如 operator<< ),我傳入 ofstream 或 ostringstream 都應(yīng)該能按預(yù)期工作,這就是 OO 繼承強(qiáng)調(diào)的“可替換性”,派生類的對(duì)象可以替換基類對(duì)象,從而被 operator<< 復(fù)用。

iostream 的繼承體系多次違反了 Liskov 原則,這些地方繼承的目的是為了復(fù)用基類的代碼,下圖中我把違規(guī)的繼承關(guān)系用紅線標(biāo)出。

correct

在現(xiàn)有的繼承體系中,合理的有:

  • ifstream is-a istream
  • istringstream is-a istream
  • ofstream is-a ostream
  • ostringstream is-a ostream
  • fstream is-a iostream
  • stringstream is-a iostream

我認(rèn)為不怎么合理的有:

  • ios 繼承 ios_base,有沒有哪種情況下程序代碼期待 ios_base 對(duì)象,但是客戶可以傳入一個(gè) ios 對(duì)象替代之?如果沒有,這里用 public 繼承是不是違反 OO 原則?
  • istream 繼承 ios,有沒有哪種情況下程序代碼期待 ios 對(duì)象,但是客戶可以傳入一個(gè) istream 對(duì)象替代之?如果沒有,這里用 public 繼承是不是違反 OO 原則?
  • ostream 繼承 ios,有沒有哪種情況下程序代碼期待 ios 對(duì)象,但是客戶可以傳入一個(gè) ostream 對(duì)象替代之?如果沒有,這里用 public 繼承是不是違反 OO 原則?
  • iostream 多重繼承 istream 和 ostream。為什么 iostream 要同時(shí)繼承兩個(gè) non-interface class?這是接口繼承還是實(shí)現(xiàn)繼承?是不是可以用組合(composition)來替代?(見《Effective C++ 第3版》條款38:通過組合模塑出 has-a 或“以某物實(shí)現(xiàn)”。《C++ 編程規(guī)范》條款 34:盡可能以組合代替繼承。)

用組合替換繼承之后的體系:

myown

注意到在新的設(shè)計(jì)中,只有真正的 is-a 關(guān)系采用了 public 繼承,其他均以組合來代替,組合關(guān)系以紅線表示。新的設(shè)計(jì)沒有用的虛擬繼承或多重繼承。

其中 iostream 的新實(shí)現(xiàn)值得一提,代碼結(jié)構(gòu)如下:

class istream;
class ostream;

class iostream
{
 public:
  istream& get_istream();
  ostream& get_ostream();
  virtual ~iostream();
};

這樣一來,在需要 iostream 對(duì)象表現(xiàn)得像 istream 的地方,調(diào)用 get_istream() 函數(shù)返回一個(gè) istream 的引用;在需要 iostream 對(duì)象表現(xiàn)得像 ostream 的地方,調(diào)用 get_ostream() 函數(shù)返回一個(gè) ostream 的引用。功能不受影響,而且代碼更清晰。(雖然我非常懷疑 iostream 的真正價(jià)值,一個(gè)東西既可讀又可寫,說明是個(gè) sophisticated IO 對(duì)象,為什么還用這么厚的 OO 封裝?)

陽春的 locale

iostream 的故事還不止這些,它還包含一套陽春的 locale/facet 實(shí)現(xiàn),這套實(shí)踐中沒人用的東西進(jìn)一步增加了 iostream 的復(fù)雜度,而且不可避免地影響其性能。Nathan Myers 正是始作俑者 http://www.cantrip.org/locale.html

ostream 自身定義的針對(duì)整數(shù)和浮點(diǎn)數(shù)的 operator<< 成員函數(shù)的函數(shù)體是:

bool failed =
  use_facet<num_put>(getloc()).put(
    ostreambuf_iterator(*this), *this, fill(), val).failed();

它會(huì)轉(zhuǎn)而調(diào)用 num_put::put(),后者會(huì)調(diào)用 num_put::do_put(),而 do_put() 是個(gè)虛函數(shù),沒辦法 inline。iostream 在性能方面的不足恐怕部分來自于此。這個(gè)虛函數(shù)白白浪費(fèi)了把 template 的實(shí)現(xiàn)放到頭文件應(yīng)得的好處,編譯和運(yùn)行速度都快不起來。

我沒有深入挖掘其中的細(xì)節(jié),感興趣的同學(xué)可以移步觀看 facet 的繼承體系:http://gcc.gnu.org/onlinedocs/libstdc++/libstdc++-html-USERS-4.4/a00431.html

據(jù)此分析,我不認(rèn)為以 iostream 為基礎(chǔ)的上層程序庫(比方說那些克服 iostream 格式化方面的缺點(diǎn)的庫)有多大的實(shí)用價(jià)值。

臆造抽象

孟巖評(píng)價(jià) “ iostream 最大的缺點(diǎn)是臆造抽象”,我非常贊同他老人家的觀點(diǎn)。

這個(gè)評(píng)價(jià)同樣適用于 Java 那一套疊床架屋的 InputStream/OutputStream/Reader/Writer 繼承體系,.NET 也搞了這么一套繁文縟節(jié)。

乍看之下,用 input stream 表示一個(gè)可以“讀”的數(shù)據(jù)流,用 output stream 表示一個(gè)可以“寫”的數(shù)據(jù)流,屏蔽底層細(xì)節(jié),面向接口編程,“符合面向?qū)ο笤瓌t”,似乎是一件美妙的事情。但是,真實(shí)的世界要?dú)埧岬枚唷?/p>

IO 是個(gè)極度復(fù)雜的東西,就拿最常見的 memory stream、file stream、socket stream 來說,它們之間的差異極大:

  • 是單向 IO 還是雙向 IO。只讀或者只寫?還是既可讀又可寫?
  • 順序訪問還是隨機(jī)訪問。可不可以 seek?可不可以退回 n 字節(jié)?
  • 文本數(shù)據(jù)還是二進(jìn)制數(shù)據(jù)。格式有誤怎么辦?如何編寫健壯的處理輸入的代碼?
  • 有無緩沖。write 500 字節(jié)是否能保證完全寫入?有沒有可能只寫入了 300 字節(jié)?余下 200 字節(jié)怎么辦?
  • 是否阻塞。會(huì)不會(huì)返回 EWOULDBLOCK 錯(cuò)誤?
  • 有哪些出錯(cuò)的情況。這是最難的,memory stream 幾乎不可能出錯(cuò),file stream 和 socket stream 的出錯(cuò)情況完全不同。socket stream 可能遇到對(duì)方斷開連接,file stream 可能遇到超出磁盤配額。

根據(jù)以上列舉的初步分析,我不認(rèn)為有辦法設(shè)計(jì)一個(gè)公共的基類把各方面的情況都考慮周全。各種 IO 設(shè)施之間共性太小,差異太大,例外太多。如果硬要用面向?qū)ο髞斫#愐刺荩ㄖ环殴残裕@個(gè)基類包含的 interface functions 沒多大用),要么太肥(把各種 IO 設(shè)施的特性都包含進(jìn)來,這個(gè)基類包含的 interface functions 很多,但是不是每一個(gè)都能調(diào)用)。

C 語言對(duì)此的解決辦法是用一個(gè) int 表示 IO 對(duì)象(file 或 PIPE 或 socket),然后配以 read()/write()/lseek()/fcntl() 等一系列全局函數(shù),程序員自己搭配組合。這個(gè)做法我認(rèn)為比面向?qū)ο蟮姆桨敢?jiǎn)潔高效。

iostream 在性能方面沒有比 stdio 高多少,在健壯性方面多半不如 stdio,在靈活性方面受制于本身的復(fù)雜設(shè)計(jì)而難以讓使用者自行擴(kuò)展。目前看起來只適合一些簡(jiǎn)單的要求不高的應(yīng)用,但是又不得不為它的復(fù)雜設(shè)計(jì)付出運(yùn)行時(shí)代價(jià),總之其定位有點(diǎn)不上不下。

在實(shí)際的項(xiàng)目中,我們可以提煉出一些簡(jiǎn)單高效的 strip-down 版本,在獲得便利性的同時(shí)避免付出不必要的代價(jià)。

一個(gè) 300 行的 memory buffer output stream

我認(rèn)為以 operator<< 來輸出數(shù)據(jù)非常適合 logging,因此寫了一個(gè)簡(jiǎn)單的 LogStream。代碼不到 300行,完全獨(dú)立于 iostream。

這個(gè) LogStream 做到了類型安全和類型可擴(kuò)展。它不支持定制格式化、不支持 locale/facet、沒有繼承、buffer 也沒有繼承與虛函數(shù)、沒有動(dòng)態(tài)分配內(nèi)存、buffer 大小固定。簡(jiǎn)單地說,適合 logging 以及簡(jiǎn)單的字符串轉(zhuǎn)換。

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 本身不是線程安全的,它不適合做全局對(duì)象。正確的使用方式是每條 log 消息構(gòu)造一個(gè) LogStream,用完就扔。LogStream 的成本極低,這么做不會(huì)有什么性能損失。

目前這個(gè) logging 庫還在開發(fā)之中,只完成了 LogStream 這一部分。將來可能改用動(dòng)態(tài)分配的 buffer,這樣方便在線程之間傳遞數(shù)據(jù)。

整數(shù)到字符串的高效轉(zhuǎn)換

muduo::LogStream 的整數(shù)轉(zhuǎn)換是自己寫的,用的是 Matthew Wilson 的算法,見 http://blog.csdn.net/solstice/article/details/5139302 。這個(gè)算法比 stdio 和 iostream 都要快。

浮點(diǎn)數(shù)到字符串的高效轉(zhuǎn)換

目前 muduo::LogStream 的浮點(diǎn)數(shù)格式化采用的是 snprintf() 所以從性能上與 stdio 持平,比 ostream 快一些。

浮點(diǎn)數(shù)到字符串的轉(zhuǎn)換是個(gè)復(fù)雜的話題,這個(gè)領(lǐng)域 20 年以來沒有什么進(jìn)展(目前的實(shí)現(xiàn)大都基于 David M. Gay 在 1990 年的工作《Correctly Rounded Binary-Decimal and Decimal-Binary Conversions》,代碼 http://netlib.org/fp/),直到 2010 年才有突破。

Florian Loitsch 發(fā)明了新的更快的算法 Grisu3,他的論文《Printing floating-point numbers quickly and accurately with integers》發(fā)表在 PLDI 2010,代碼見 Google V8 引擎,還有這里 http://code.google.com/p/double-conversion/ 。有興趣的同學(xué)可以閱讀這篇博客 http://www.serpentine.com/blog/2011/06/29/here-be-dragons-advances-in-problems-you-didnt-even-know-you-had/

將來 muduo::LogStream 可能會(huì)改用 Grisu3 算法實(shí)現(xiàn)浮點(diǎn)數(shù)轉(zhuǎn)換。

性能對(duì)比

由于 muduo::LogStream 拋掉了很多負(fù)擔(dān),可以預(yù)見它的性能好于 ostringstream 和 stdio。我做了一個(gè)簡(jiǎn)單的性能測(cè)試,結(jié)果如下。

benchmark

從上表看出,ostreamstream 有時(shí)候比 snprintf 快,有時(shí)候比它慢,muduo::LogStream 比它們兩個(gè)都快得多(double 類型除外)。

泛型編程

其他程序庫如何使用 LogStream 作為輸出呢?辦法很簡(jiǎn)單,用模板。

前面我們定義了 Date class 針對(duì) std::ostream 的 operator<<,只要稍作修改就能同時(shí)適用于 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;
 }

現(xiàn)實(shí)的 C++ 程序如何做文件 IO

舉兩個(gè)例子, Kyoto CabinetGoogle leveldb

Google leveldb

Google leveldb 是一個(gè)高效的持久化 key-value db。

它定義了三個(gè)精簡(jiǎn)的 interface:

接口函數(shù)如下

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 明確區(qū)分 input 和 output,進(jìn)一步它又把 input 分為 sequential 和 random access,然后提煉出了三個(gè)簡(jiǎn)單的接口,每個(gè)接口只有屈指可數(shù)的幾個(gè)函數(shù)。這幾個(gè)接口在各個(gè)平臺(tái)下的實(shí)現(xiàn)也非常簡(jiǎn)單明了(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),一看就懂。

注意這三個(gè)接口使用了虛函數(shù),我認(rèn)為這是正當(dāng)?shù)模驗(yàn)橐淮?IO 往往伴隨著 context switch,虛函數(shù)的開銷比起 context switch 來可以忽略不計(jì)。相反,iostream 每次 operator<<() 就調(diào)用虛函數(shù),我認(rèn)為不太明智。

Kyoto Cabinet

Kyoto Cabinet 也是一個(gè) key-value db,是前幾年流行的 Tokyo Cabinet 的升級(jí)版。它采用了與 leveldb 不同的文件抽象。

KC 定義了一個(gè) File class,同時(shí)包含了讀寫操作,這是個(gè) fat interface。http://fallabs.com/kyotocabinet/api/classkyotocabinet_1_1File.html

在具體實(shí)現(xiàn)方面,它沒有使用虛函數(shù),而是采用 #ifdef 來區(qū)分不同的平臺(tái)(見 http://code.google.com/p/read-taobao-code/source/browse/trunk/tair/src/storage/kdb/kyotocabinet/kcfile.cc),等于把兩份獨(dú)立的代碼寫到了同一個(gè)文件里邊。

相比之下,Google leveldb 的做法更高明一些。

小結(jié)

在 C++ 項(xiàng)目里邊自己寫個(gè) File class,把項(xiàng)目用到的文件 IO 功能簡(jiǎn)單封裝一下(以 RAII 手法封裝 FILE* 或者 file descriptor 都可以,視情況而定),通常就能滿足需要。記得把拷貝構(gòu)造和賦值操作符禁用,在析構(gòu)函數(shù)里釋放資源,避免泄露內(nèi)部的 handle,這樣就能自動(dòng)避免很多 C 語言文件操作的常見錯(cuò)誤。

如果要用 stream 方式做 logging,可以拋開繁重的 iostream 自己寫一個(gè)簡(jiǎn)單的 LogStream,重載幾個(gè) operator<<,用起來一樣方便;而且可以用 stack buffer,輕松做到線程安全。

posted on 2011-07-17 15:06 陳碩 閱讀(12567) 評(píng)論(10)  編輯 收藏 引用

評(píng)論

# re: C++ 工程實(shí)踐(7):iostream 的用途與局限 2011-07-17 15:45 千暮(zblc)

mark.  回復(fù)  更多評(píng)論   

# re: C++ 工程實(shí)踐(7):iostream 的用途與局限 2011-07-17 16:18 fool

文章寫得非常棒。揪個(gè)小問題:

“C 語言對(duì)此的解決辦法是用一個(gè) int 表示 IO 對(duì)象(file 或 PIPE 或 socket),然后配以 read()/write()/lseek()/fcntl() 等一系列全局函數(shù),程序員自己搭配組合。這個(gè)做法我認(rèn)為比面向?qū)ο蟮姆桨敢?jiǎn)潔高效。”

file descriptor 配以 read/write 的方案是Unix的system calls,不屬于C。FILE* 加上 fread/fwrite 才是C的標(biāo)準(zhǔn)庫。  回復(fù)  更多評(píng)論   

# re: C++ 工程實(shí)踐(7):iostream 的用途與局限 2011-07-17 17:32 right

文章很棒,有理有據(jù),調(diào)理分明。  回復(fù)  更多評(píng)論   

# re: C++ 工程實(shí)踐(7):iostream 的用途與局限 2011-07-17 20:02 空明流轉(zhuǎn)

locale是必要的。  回復(fù)  更多評(píng)論   

# re: C++ 工程實(shí)踐(7):iostream 的用途與局限 2011-07-17 22:26 陳梓瀚(vczh)

使用微軟專有編譯器VC++,只要你把stl頭文件放進(jìn)precompiled header(stdafx),保證編譯飛快(因?yàn)閟tl顯然是不會(huì)變化的——相對(duì)于你自己的代碼而言,因此可以犧牲一點(diǎn)點(diǎn)硬盤空間做cache)。

話說,stl算什么重量級(jí)呢,就是#include一行而已啊。編譯器怎么處理你不用管,因?yàn)檫@跟C++的語義沒關(guān)系。如果你怕慢,那么使用precompiled header。  回復(fù)  更多評(píng)論   

# re: C++ 工程實(shí)踐(7):iostream 的用途與局限 2011-07-17 22:28 陳梓瀚(vczh)

而且stl的設(shè)計(jì)總體更傾向于函數(shù)式和組合子。stl的設(shè)計(jì)者都說他自己不用OO,因此當(dāng)然會(huì)違反很多OO的原則拉——因?yàn)楸緛砭筒幌隣O的。  回復(fù)  更多評(píng)論   

# re: C++ 工程實(shí)踐(7):iostream 的用途與局限 2011-07-18 10:20 空明流轉(zhuǎn)

@陳梓瀚(vczh)

主流編譯器都有pre compiled功能,射射。  回復(fù)  更多評(píng)論   

# re: C++ 工程實(shí)踐(7):iostream 的用途與局限 2011-07-18 13:09 yrj

專用的庫可以優(yōu)化到極致,既通用有靈活高效高性能的庫不容易。
博主舉得“現(xiàn)實(shí)的 C++ 程序如何做文件 IO”的兩個(gè)例子只是 IO 并沒有解決 iostream 的問題。

@vczh
我記得 STL 和 iostream 設(shè)計(jì)思想是不同的。  回復(fù)  更多評(píng)論   

# re: C++ 工程實(shí)踐(7):iostream 的用途與局限 2011-07-19 23:31 flyinghearts



文章太長(zhǎng)了,粗略的看了下,說下自己的淺薄看法:

ssize_t等幾個(gè)類型不是標(biāo)準(zhǔn)C定義的。flockfile、funlockfile也不是C標(biāo)準(zhǔn)自帶的。

關(guān)于線程安全問題:
  C/C++標(biāo)準(zhǔn)都不能保證fprintf/iostream線程安全吧。fprintf能否線程安全,取決于編譯器是否提供了多線程安全的庫,在mingw-gcc上它就不是線程安全的。iostream可以通過手動(dòng)加解鎖實(shí)現(xiàn)線程安全,多線程安全的stdio庫同樣要加解鎖,只不過是由庫自動(dòng)加解鎖。用戶手動(dòng)加解鎖雖然麻煩點(diǎn),但是更靈活。


關(guān)于整數(shù)到字符串的高效轉(zhuǎn)換:
 那個(gè)算法不但看起來別扭,還依賴于某個(gè)語言的具體規(guī)定,能不用最好不用。

 整數(shù)到字符串的轉(zhuǎn)換算法:先判斷“數(shù)的正負(fù)號(hào)”,然后取絕對(duì)值,并將“有符號(hào)數(shù)”轉(zhuǎn)為“無符號(hào)數(shù)”( 保證對(duì)最小負(fù)數(shù)取絕對(duì)值的結(jié)果是正確的),再進(jìn)行“無符號(hào)數(shù)”乘除法計(jì)算確定每一位上的數(shù)字。

最好將
  int lsd = i % 10; // lsd 可能小于 0
  i /= 10; // 是向下取整還是向零取整?
改成寫:
int lsd = i % 10;
int quotient = i / 10;
i = quotient;
對(duì)GCC,不指定-march=i686的話,前一種寫法很可能比后一種要多進(jìn)行一次除法計(jì)算(取模運(yùn)算)!


弄個(gè)臨時(shí)數(shù)組保存結(jié)果,再一次拷到目標(biāo)數(shù)組,好像也能提升點(diǎn)性能。



關(guān)于庫的代碼:
  太依賴于boost,雖然機(jī)器上已經(jīng)裝了boost,但還是出現(xiàn)編譯問題。
  對(duì)int/long/long long,這三個(gè)類型formatInteger都要實(shí)例化,有點(diǎn)浪費(fèi),可以通過類型轉(zhuǎn)換,在32/64位機(jī)子上,只實(shí)例化其中的2/1個(gè)。
 
  回復(fù)  更多評(píng)論   

# re: C++ 工程實(shí)踐(7):iostream 的用途與局限 2011-08-23 10:23 pyphehe

非常好的文章。我也在設(shè)計(jì)自己的file讀寫接口,看了之后很有啟發(fā)!感謝博主!  回復(fù)  更多評(píng)論   


只有注冊(cè)用戶登錄后才能發(fā)表評(píng)論。
網(wǎng)站導(dǎo)航: 博客園   IT新聞   BlogJava   博問   Chat2DB   管理


<2011年7月>
262728293012
3456789
10111213141516
17181920212223
24252627282930
31123456

導(dǎo)航

統(tǒng)計(jì)

常用鏈接

隨筆分類

隨筆檔案

相冊(cè)

搜索

最新評(píng)論

閱讀排行榜

評(píng)論排行榜

青青草原综合久久大伊人导航_色综合久久天天综合_日日噜噜夜夜狠狠久久丁香五月_热久久这里只有精品
  • <ins id="pjuwb"></ins>
    <blockquote id="pjuwb"><pre id="pjuwb"></pre></blockquote>
    <noscript id="pjuwb"></noscript>
          <sup id="pjuwb"><pre id="pjuwb"></pre></sup>
            <dd id="pjuwb"></dd>
            <abbr id="pjuwb"></abbr>
            蜜臀av性久久久久蜜臀aⅴ| 久久久午夜电影| 亚洲另类春色国产| 欧美另类一区二区三区| 一区二区日韩欧美| 亚洲一区二区三区国产| 国产视频精品免费播放| 蜜臀av一级做a爰片久久| 蜜桃av噜噜一区二区三区| 亚洲精品系列| 亚洲一区二区三区精品动漫| 狠狠色香婷婷久久亚洲精品| 亚洲国产一区二区a毛片| 欧美福利一区二区| 新狼窝色av性久久久久久| 久久精品伊人| 亚洲老司机av| 欧美在线999| 亚洲欧洲视频| 亚洲女女女同性video| 国户精品久久久久久久久久久不卡| 欧美波霸影院| 国产精品美女999| 噜噜噜躁狠狠躁狠狠精品视频| 欧美精品 国产精品| 欧美在线一级va免费观看| 欧美sm极限捆绑bd| 欧美伊人久久大香线蕉综合69| 欧美jizzhd精品欧美喷水| 午夜久久电影网| 欧美激情一区二区三区全黄 | 美女视频黄 久久| 亚洲欧美在线免费| 欧美电影在线观看完整版| 久久女同精品一区二区| 国产精品成人国产乱一区| 欧美成人午夜影院| 国产一区再线| 亚洲综合日韩| 中文精品一区二区三区 | 欧美在线视频网站| 亚洲男人天堂2024| 欧美女同在线视频| 欧美激情精品久久久久| 国产中文一区二区| 亚洲欧美激情视频在线观看一区二区三区 | 欧美精选一区| 欧美激情中文字幕在线| 一区二区视频免费完整版观看| 亚洲午夜电影网| 亚洲图片欧美一区| 欧美日韩在线大尺度| 亚洲欧洲一区二区在线播放 | 欧美第十八页| 欧美国产视频日韩| 亚洲激情国产| 欧美a级一区| 亚洲福利视频在线| 亚洲日本va在线观看| 久久琪琪电影院| 欧美成年人在线观看| 精品91在线| 久久久一区二区| 欧美激情aⅴ一区二区三区| 在线观看精品| 欧美成人黄色小视频| 亚洲高清在线观看一区| 亚洲免费电影在线| 欧美日韩无遮挡| 正在播放亚洲| 久久久久国产成人精品亚洲午夜| 国产欧美日韩专区发布| 欧美中文日韩| 欧美国产日韩免费| 一区二区三区高清| 国产精品va在线播放| 亚洲一区影音先锋| 国产九区一区在线| 国产精品99免费看| 99re热精品| 羞羞答答国产精品www一本| 国产视频久久久久久久| 久久精品三级| 亚洲人精品午夜| 亚洲一区精品电影| 国产一区久久久| 欧美高清一区| 亚洲欧美成aⅴ人在线观看| 久久久亚洲高清| 亚洲美女淫视频| 国产麻豆午夜三级精品| 久久久噜噜噜久久久| 日韩视频免费观看高清在线视频| 先锋影音久久| 亚洲国产导航| 国产精品久久久久久亚洲毛片| 久久精品国产免费看久久精品| 亚洲国产美女| 久久久久.com| 亚洲一二三四久久| 亚洲第一区在线| 国产精品久久久久久久7电影| 久久性天堂网| 亚洲小说欧美另类婷婷| 欧美国产激情| 久久久久久久综合日本| 一区二区三区日韩欧美精品| 国语自产精品视频在线看抢先版结局 | 一本色道久久综合狠狠躁篇的优点| 久久精品盗摄| 亚洲视频在线一区| 91久久精品一区二区三区| 国产乱码精品1区2区3区| 欧美~级网站不卡| 久久精品国产一区二区三| 一区二区免费在线观看| 亚洲国产高清aⅴ视频| 久久精品国产欧美激情| 亚洲性视频h| 99精品欧美一区二区三区| 黄色成人av网站| 国产欧美日韩精品一区| 欧美视频一区二区三区…| 欧美成人一品| 欧美www在线| 老牛国产精品一区的观看方式| 性做久久久久久| 亚洲女女女同性video| 亚洲视频在线看| 日韩一区二区免费高清| 亚洲人成人一区二区在线观看| 麻豆国产精品一区二区三区 | 亚洲精品中文字幕在线观看| 亚洲第一狼人社区| 亚洲国产成人91精品| 激情六月婷婷综合| 一区在线电影| 亚洲国产成人一区| 91久久久久| 亚洲精品久久久久久一区二区| 最新成人av在线| 亚洲精品视频在线看| 亚洲美女电影在线| 夜夜狂射影院欧美极品| 一区二区激情| 中日韩高清电影网| 亚洲欧美精品一区| 欧美在线视频a| 亚洲精品中文字| 暖暖成人免费视频| 欧美黑人国产人伦爽爽爽| 欧美大片在线看| 亚洲人成网站色ww在线| 亚洲精选视频在线| 亚洲淫片在线视频| 久久精品国产一区二区三区| 蜜桃av噜噜一区二区三区| 欧美精品18| 国产精品亚洲网站| 在线观看成人一级片| 亚洲国产精品日韩| 亚洲午夜影视影院在线观看| 欧美在线free| 欧美二区视频| 亚洲午夜在线观看| 久久三级视频| 欧美性做爰毛片| 国内综合精品午夜久久资源| 亚洲三级毛片| 欧美伊人久久| 亚洲黄色成人| 小黄鸭精品aⅴ导航网站入口| 久久乐国产精品| 欧美性jizz18性欧美| 极品av少妇一区二区| 亚洲网站视频福利| 免费国产一区二区| 一区二区三区www| 久久米奇亚洲| 国产精品国产馆在线真实露脸| 在线精品视频在线观看高清| 在线视频亚洲欧美| 欧美成人高清| 香蕉尹人综合在线观看| 欧美人妖在线观看| 亚洲福利国产精品| 久久国产加勒比精品无码| 91久久精品日日躁夜夜躁欧美| 欧美在线视频导航| 欧美午夜视频在线| 亚洲精品国产精品国自产观看浪潮| 欧美一级电影久久| 99爱精品视频| 欧美肥婆在线| 一区二区在线免费观看| 久久国产精品99久久久久久老狼| 亚洲美女在线视频| 欧美激情成人在线视频| 亚洲国产另类久久久精品极度| 久久av红桃一区二区小说|