muduo網路庫學習筆記(15):關於使用stdio和iostream的討論

li27z發表於2017-05-28

C/C++程式中需要執行輸入/輸出時,我們一般會用到stdio或iostream。stdio指C語言的scanf/printf系列格式化輸入輸出函式,iostream指C++語言的cin/cout輸入輸出物件等。

但是,在真實的專案中很少用到iostream(muduo網路庫也不例外),本篇就對二者的優、缺點進行一個小結(主要考慮x86 Linux平臺,不考慮跨平臺的可移植性,但是要考慮32-bit和64-bit的相容性)。

stdio

(一)缺點:對程式設計初學者不友好
我們在C語言入門時、在輸出第一行“Hello World”程式碼時,用到了標準輸入輸出stdio,而在學習C++語言的過程中,我們又接觸到了iostream庫的輸入輸出機制。相對於iostream給初學者提供了一個方便的命令列輸入輸出實驗環境,stdio對於初學者來說則繁瑣很多,看下面這個示例:

#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\n", i, s, f, d, name);
  return 0;
}

可以看到:
1.輸入和輸出用的格式字串不一樣。輸入short要用%hd,輸出用%d;輸入double要用%lf,輸出用%f。
2.輸入的引數不統一。對於i,s,f,d等變數,在傳入scanf()的時候要取地址(&);而對於字元陣列name,則不用取地址。
3.程式有緩衝區溢位的危險。上面的例子在讀入name的時候沒有指定大小,這是用C語言程式設計的安全漏洞的主要來源。

向剛開始學程式設計的初學者清楚解釋這幾條背後的原因可謂是相當困難(涉及傳遞函式不定引數時的型別轉換、函式呼叫棧的記憶體佈局、指標的意義、字元陣列退化為字元指標等等)。

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;
  return 0;
}

這段程式碼對於初學者來說更易懂,而且沒有安全性方面的問題。

(二)缺點:安全性問題
輸出方面的安全性問題,可用snprintf()等能夠指定輸出緩衝區大小的函式來解決;但輸入方面似乎沒有太大的進展,例如需要從檔案或標準輸入讀入一行不確定長度的字串,C語言標準庫函式gets()、fgets()和getline()都不能完美地完成這個任務,還要靠程式設計師自己動手(一種讀取不定長字串輸入的實現:https://segmentfault.com/a/1190000000360944)。

另外,引數型別繁多導致的型別安全問題也不容忽視。如果printf()的整數引數型別是int、long等內建型別,那麼printf()的格式化字串很容易寫,但是如果引數是系統檔案裡typedef的型別呢,例如clock_t、in_addr_t、pid_t等?

這些問題在C++裡都不存在,在這方面iostream是個進步。

(三)缺點:不可擴充套件
舉例來說:

#include <stdio.h>

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

int main()
{
  Date date;
  printf("%D\n", &date);  // 錯誤
  return 0;
}

即C stdio無法支援自定義的型別,iostream則可通過函式過載來實現可擴充套件性,如下:

#include <iostream>

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(2017, 5, 28);
  std::cout << date << std::endl;
  return 0;
}

(四)優點:通用性廣
在C語言之外,有其他很多語言也支援printf()風格的格式化。學會 printf() 的格式化方法,這個知識還可以用到其他語言中。但是 C++ iostream 只此一家別無分店,反正都是格式化輸出,stdio 的投資回報率更高。

所以,我們不必深究 iostream 的格式化方法,只需要用好它最基本的型別安全輸出即可。在真的需要格式化的場合,可以考慮 snprintf() 列印到棧上緩衝,再用 ostream 輸出。

(五)優點:外部可配置性強
比方說,我想用一個外部的配置檔案來定義日期的格式。C stdio很好辦,把格式字串”%d-%02d-%02d”儲存到配置裡就行。但是iostream呢?它的格式是寫死在程式碼裡的,靈活性大打折扣。

iostream

通過以上的討論,我們可以知道iostream相對於stdio具有型別安全、型別可擴充套件等優點。但是深入一點,就會發現iostream在使用和設計方面的不可避免的缺點,“瑜不掩瑕”。以下是iostream在使用方面的一些缺點:
(一)輸入方面,istream不適合輸入帶格式的資料,因為“糾錯”能力不強。

(二)輸出方面,格式化輸出很繁瑣。舉一個例子來說明,以“2017-05-28”的格式來輸出前面定義的Date class:

// 用iostream
void writeTo(std::ostream& os)const
{
  os << year_ << '-'
     << std::setw(2) << std::setfill('0') << month_ << '-'
     << std::setw(2) << std::setfill('0') << day_;
}
// 用stdio
void writeTo(std::ostream& os)const
{
  char buf[32];
  snprintf(buf, sizeof buf, "%d-%02d-%02d", year_, month_, day_);
  os << buf;
}

正如前面的第(四)點所言。

(三)stream的狀態易受影響。例如,我們想輸出一個16進位制的數字x,那麼可以用 hex 操控符,但是這會改變 ostream 的狀態:

// 這段程式碼會將數字123也以16進位制的方式輸出,這恐怕不是我們想要的
int x = 666;
cout << hex << showbase << x << endl;  // forget to reset state
cout << 123 << endl;

再舉一個例子,setprecision() 也會造成持續影響:

double d = 123.45;
printf("%8.3f\n", d);  // 輸出:123.450
cout << d << endl;  // 輸出:123.45
cout << setw(8) << fixed << setprecision(3) << d << endl;  // 輸出:123.450
cout << d << endl;  // 輸出:123.450

可見程式碼中的setprecision()影響了後續輸出的精度。注意:setw()不會造成影響,它只對下一個輸出有效。

這說明,如果使用manipulator來控制格式,需要時刻小心防止影響了後續程式碼。而使用C stdio就沒有這個問題,它是“上下文無關的”。

(四)執行緒安全方面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在某些場合比 stdio快,在某些場合比stdio慢,對於效能要求較高的場合,我們應該自己實現字串轉換。(tips:線上 ACM/ICPC 判題網站上,如果一個簡單的題目發生超時錯誤,那麼把其中iostream的輸入輸出換成stdio,有時就能過關)

因此,iostream在實際專案中的應用就大為受限了。


參考資料:
C++ 工程實踐(7):iostream 的用途與侷限(博文對iostream的侷限做了更全面、深層次的剖析,值得一看)
http://www.cnblogs.com/Solstice/archive/2011/07/17/2108715.html

相關文章