小心為上:注意C++ fstream給你設下的陷阱
小心為上:注意C++ fstream給你設下的陷阱
透過名字看本質:到底什麼是stream?
stream的定義
stream的中文翻譯為“流”,不是很好理解,我們來看英文關於stream的定義,比較常見的有兩個:
1. A stream is an abstraction that represents a device on which input and output operations are performed.
2. A stream is a "stream of data" in which character sequences "flow."
英文看起來比較累,總結一下,提煉如下幾個關鍵點:
1. abstraction that represents a device:代表一個設備;
2. stream of data:數據流,隱含了FIFO的一個特性;
3. character sequences flow:字符序列在其中流動,而不是二進製在其中流動;
多說無益,還是看圖說話:
注:上圖中暗含一個玄機:stream的數據是設備數據的一個子集,因為stream隻是代表一個設備,而並不是完全等於一個設備。
關公戰秦瓊: stream vs buffer
一個容易和stream混淆的概念就是大家常見的buffer,buffer的定義為(參見WIKI Data Buffer):A buffer is a region of memory used to temporarily hold data while it is being moved from one place to another.
英文看起來也比較累,還是總結一下:
1. region of memory:一塊內存區域,因此就隱含了隨機訪問和二進製的操作方式;
2. one place to another:數據移動過程中使用
經過對兩個術語定義的分析總結,相信大家都明白了這兩者之間的差異了,其實這就是一個關公戰秦瓊的例子,因為它們兩個實際上並不是一個競爭關係。
匯總對比點如下:
對比點 |
stream |
buffer |
作用 |
代表一個設備 |
數據臨時存儲 |
訪問方式 |
FIFO |
隨機訪問 |
數據內容 |
字符流 |
二進製 |
當然,並不是說stream和buffer就毫無關係了,stream為了提高性能,實現的時候就用到了buffer。
小心為上:注意fstream設下的陷阱
2.1 char和wchar_t的操作
當你使用<<輸出的時候,任何字符都可以輸出;但當使用>>進行輸入的時候,開頭的空白字符缺省情況下是跳過的。
例如:假設文件中有這麼一個字符串“ test”,則使用>>讀入的時候,直接就讀到了字母‘t’,而不會是空格。
避免這個陷阱的招數:
1. 清除skipws標誌:unsetf(ios::skipws)
2. 使用get()函數
2.2 指針類型的操作
1. char*
char*類型的數據輸出時會將字符串全部輸出,但輸入的時候你千萬要注意,輸入的時候默認會將前麵的空白字符全部去掉;而且輸入時默認是以空格來作為分隔符的,例如:“This is a test C-string.”可以全部輸出,但如果文件中有這麼一串字符串,那麼用<<是無法全部讀入的,隻能讀入“This”“is”“a”“test”“C-string”.
避免這個陷阱的招數:
1. 如果想改變默認去掉頭部空白字符的操作方式,請參考skipws
2. 如果想改變以空格作為結束符的操作方式,對不起,用<<是沒有辦法的,隻能用istream& istream::get (char* str, streamsize count, char delim)或者
istream& istream::getline (char* str, streamsize count, char delim)
2. void*
直接輸出指針地址。
3. 其它指針
不管是指向int/double等標準數據類型,還是自定義的struct/class類型,都是輸出指針指向的地址,而不是輸出指針指向的對象。
2.3 eof和fail
2.3.1 eof
eof從字麵意思來看,當然是end of file,用於表明當前已經到了文件末尾,不能再讀了。
但這裏有一個很迷惑的陷阱:隻要遇到結束符,流就會將狀態置為EOF,而不管置位前的操作是否成功。
例如,使用getline函數讀取文件的最後一行,如果這一行是因為遇到了EOF而結束的,那麼getline操作是成功的,但eof還是會置位。
因此,不能在調用函數後通過eof來判斷函數調用是否讀到文件末尾了,而應該直接判斷調用本身是否成功,具體樣例請看fail。
2.3.2 fail
導致fail標誌位置位的有如下常見的情況:
1. 文件不存在;
2. 文件不能創建;
3. eof標誌位置位;
4. 非法的格式,例如當你期望數字的時候,而文件裏麵卻是字母
注意第三種情況,在文件eof的時候也會同時置fail,所以,循環讀取文件的時候,要將fail和eof結合起來使用:在循環判斷中使用fail,fail失敗後再使用eof。
錯誤的用法:
1: std::ifstream file("test.txt");
2: std::string word;
3: double value;
4: while ( true ) {
5: // A word and a double value were both read successfully
6 file >> word >> value;
7 if( file.eof() )
8 break;
9: }
正確的用法
1: std::ifstream file("test.txt");
2: std::string word;
3: double value;
4: while (file >> word >> value) {
5: // A word and a double value were both read successfully
6: }
7: if (!file.eof()) throw std::runtime_error("Invalid data from file");
2.4 fstream的binary打開方式
文件流的打開方式中有一個binary,從字麵意思來看,應該是按照二進製打開文件,然後進行二進製讀寫。
然而這樣理解的話,就陷入了C++的陷阱:binary實際上和二進製讀寫沒有關係,binary隻是為了告訴係統是否將不同操作係統間特定的字符替換,最典型的是換行符,在windows上是/r/n,而在Unix類係統上是/n,如果加了這個binary標誌,流就不會自動替換。
那C++的fstream流如何進行二進製讀寫呢?其實很簡單:隻需要調用get/read和put/write即可。也就是說:是否是二進製讀寫和文件打開方式無關,而是和調用函數有關。如果使用<<和>>,則就是按照字符流進行讀寫;如果使用get/read和put/write,則按照二進製讀寫。
前麵說過流是character sequences flow,那為什麼這裏又說fstream能夠按照二進製讀寫呢?由於沒有研究過相關的實現代碼,因此這裏無法給出分析,個人推斷應該是流的內部實現將字符流轉換為二進製了。
最後更新:2017-04-02 06:51:29