為什么C++和C的標準輸入輸出不同步時,數據會混亂?同步會帶來多大性能損失?為什么說這個損失通常不用太在乎?
0. 課堂視頻
C++之“流”-第2課:和C輸入輸出的同步
1. 理解cin和cout的類型與創建過程
std::cout 是std::ostream類型的一個變量;而 std::ostream是std::basic_ostream<char>模板類的類型別名( typedef )。std::cin是std::isteram類型的一個變量,而std::istream是std::back_istream<cahr>模板類的類型別名。兩個模板類中的 “char” 參數,表明二者都是基于普通 字符(char)作為最小輸出或輸入單位。如果改為 wchar_t,則以UNICODE字符串作為基本輸入輸出單位。
正如上一節課所說,std::ostream和std::istream都是抽象概念的流,無法直接創建對應的輸出或輸入流對象。
注意,C++中的“抽象”概念,和 Java 這樣更加純粹的“面向對象”的編程語言有所不同。Java 中的“抽象”,通常使用:“什么實事都不做,只負責定要求” 的接口(interface)表達。C++中有更多不同的方式來表達抽象概念,可以同樣“什么事都不做,只負責定要求”的純虛類,也可以是“做了很多基礎的事,但禁用了特定構建方法”的方式。兩種方式的共同表現是:不讓用戶直接創建對象。
std::ostream和std::istream對外開放的構造方法,都要求一個“流緩存區/stream_buf”入參。以輸出為例,我們可以:
- 設計并實現一個內存輸出緩存區,傳入后以得到一個內存輸出流的基本功能;
- 設計并實現一個文件輸出緩存區,傳入后以得到一個文件輸出流的基本功能;
- 設計并實現一個網絡輸出緩存區,傳入后以得到一個網絡輸出流的基本功能;
那么,為std::istream的構造函數傳入一個鍵盤輸入流緩存區,就能得到一個標準輸入流,即std::cin;而為std::ostream傳入一個屏幕輸出流緩存區,就能得到一個標準輸出流,即std::cout。但實際上,C++程序中的std::cin和std::cout對象,都是C++庫自動創建出來的,并且不允許用戶手工創建二者。為什么呢?因為對一個程序來說,標準輸入設施應該只有一個,標準輸出設施也應該只有一個;如果用戶自己創建,就擋不住有用戶創建出一打標準輸入流或標準輸出流了。
在Windows的控制臺(console)或Linux下的終端(terminal)里,鍵盤被稱為程序的標準輸入設備,屏幕被稱為程序的標準輸出。并且,無論一臺電腦接多少個鍵盤(少見),在邏輯上都會被當作一個鍵盤;同理,無論一臺電腦接多少個屏幕(常見),在邏輯上也都會被當作一個屏幕。因此,cin 和 cout 本質上是一種“單例”,即整個程序中,只能一個標準輸入流,一個標準輸出流。
這種“一個程序里,某種類型的對象只有一個”的邏輯的實現,有專門的,稱為“單例模式”的設計模式來實現。C++實現 cin 和 cout 的單例保障倒很簡單:使用默認構造函數(沒有任何入參)來創建特定對象,再把該默認構造函數的訪問權限設置為私有(private)或保護的(protected),在gc++的實現中使用的是后者。標準庫內部可以通過 “友元”加“派生”的方式,實現對基類受保護的默認構造函數的調用。
一旦調用std::ostream的默認構造函數,由于沒有入參,也就沒有外部傳入的輸出緩存區,此時標準庫將自動創建標準輸出流的緩存區,從而創建出標準輸出流,即:std::cout對象。標準輸入流的創建過程與此類型,同樣是調用默認構造函數,然后自行創建、關聯和鍵盤輸入緩存區,從而創建出 std::cin。
以上調用過程都是在程序主函數 main() 開始之前,就執行完畢,因此我們的程序在一開始就能夠方便地使用std::cin和std::cout。事實上,在 main() 之前我們就可以使用了。如果在 main() 函數之前就開始執特定代碼,這是C++的另一個知識點,不在此講解。
2. 數據輸入輸出次序沖突問題的出現
到現在,一切看起來很完美:cin和cout是自動創建的,并且各自只會有一份,不會沖突……但是,考慮到C++的一個重要的歷史使命:兼容C語言,問題就來了——
C 語言有自己的輸入輸出機制,并且本質上,底層也需要用到輸入或輸出緩存區。上一節課我們說過,這個緩存區本質是一個數據隊列,一個“有次序保障”的數據隊列。C++盡管做到了一個程序只有一個C++輸入流或一個C++輸出流,但加上C的隊列,現在,一個C++程序會有兩個輸入隊列、兩個輸出隊列。
這就有點像現實生活中的某種排隊現象:入口或出口只有一個,但人們排了兩條隊,兩條隊伍各自的內部數據都有次序保障,但是,當門就在眼前,兩條隊伍如何通過一個門呢?無論是互相禮讓,還是爭先搶后,都無法保障復原原始的數據次序。
3. 混合輸入,同步對比不同步
代碼:
#include <cstdio> // C 語言的標準輸入輸出庫
#include <iostream>using namespace std;int main()
{ios_base::sync_with_stdio(false); // 不同步!!!int i, j;scanf("%d", &i); //用C的方式輸入 icin >> j; // 用C++的方式輸入 jcout << i << ", " << j << endl;return 0;
}
4. 混合輸出,同步對比不同步
代碼:
#include <cstdio>
#include <iostream>using namespace std;int main()
{ios_base::sync_with_stdio(false);for (int i=0; i<3; ++i){printf("hello from printf!\n");cout << "hello from cout.\n";}return 0;
}
5. 同步與不同步性能對比
代碼:
#include <ctime>
#include <cstdio>
#include <iostream>using namespace std;int main()
{ios::sync_with_stdio(false);clock_t beg = clock();for (int i=0; i<30000; ++i){cout << "hello world.";}clock_t end = clock();cout << "\n" << (end - beg) * 1000 / CLOCKS_PER_SEC << "ms." <<endl;return 0;
}
注意,程序使用 sync_with_stdio(false) 取消 C++和C的標準輸入輸出同步,該操作是不可逆的,即后續無法通過 sync_with_stdio(true) 恢復 同步。
6. 為什么不用太在乎C++標準輸入輸出的性能?
C++常用以寫以下程序:
類型 | 典型應用 | 描述 | 大致占比 | 輸入輸出性能 |
---|---|---|---|---|
后臺服務或底層組件 | 網絡服務、防火墻 | 不直接面向用戶,不使用標準輸入輸出 | 25% | 不在乎 |
GUI程序 | Photoshop、Office、游戲 | 使用系統GUI作為輸入輸出 | 20% | 不在乎 |
基礎工具 | 命令行文件處理工具:壓縮、圖片處理 | 雖然在命令行運行,但幾乎沒有輸入輸出 | 15% | 不在乎 |
簡單命令行工具 | 各類命令行客戶端程序:libcurl,文件列表 | 低頻使用標準輸入輸出 | 20% | 不在乎 |
非性能敏感的控制臺應用 | 用戶開發的簡單命令行應用,比如處理excel表格 | 性能不敏感 | 15% | 不在乎 |
性能敏感的控制臺應用 | 信息學競賽程序、遠程日志監控等 | 性能敏感,大量標準輸入輸出操作會影響程序性能 | 5% | 在乎 |