《C++ Primer》學習筆記(八):標準 IO 庫

我是管小亮發表於2020-02-05

歡迎關注WX公眾號:【程式設計師管小亮】

專欄C++學習筆記

《C++ Primer》學習筆記/習題答案 總目錄

——————————————————————————————————————————————————————

?? Cpp-Prime5 + Cpp-Primer-Plus6 原始碼和課後題

標準 IO 庫

部分IO庫設施:

  • istream:輸入流型別,提供輸入操作。
  • ostream:輸出流型別,提供輸出操作。
  • cinistream 物件,從標準輸入讀取資料。
  • coutostream 物件,向標準輸出寫入資料。
  • cerrostream 物件,向標準錯誤寫入資料。
  • >> 運算子:從 istream 物件讀取輸入資料。
  • << 運算子:向 ostream 物件寫入輸出資料。
  • getline 函式:從 istream 物件讀取一行資料,寫入 string 物件。

1、IO類

標頭檔案 iostream 定義了用於讀寫流的基本型別,fstream 定義了讀寫命名檔案的型別,sstream 定義了讀寫記憶體中 string 物件的型別。
在這裡插入圖片描述
寬字元版本的IO型別和函式的名字以 w 開始,如 wcinwcoutwcerr 分別對應 cincoutcerr。它們與其對應的普通 char 版本都定義在同一個標頭檔案中,如標頭檔案 fstream 定義了 ifstreamwifstream 型別。

通常可以將一個派生類(繼承類)物件當作其基類(所繼承的類)物件來使用,這是通過 繼承機制(inheritance) 實現的。

1)IO象無拷貝或賦值

不能拷貝或對IO物件賦值。

ofstream out1, out2;
out1 = out2;    			// 錯誤:不能對流物件賦值
ofstream print(ofstream);   // 錯誤:不能初始化ofstream引數
out2 = print(out2);     	// 錯誤:不能拷貝流物件

由於IO物件不能拷貝,因此不能將函式形參或返回型別定義為流型別。進行IO操作的函式通常以引用方式傳遞和返回流。讀寫一個IO物件會改變其狀態,因此傳遞和返回的引用不能是 const 的。

2)條件狀態

IO操作一個與生俱來的問題是可能發生錯誤。一些錯誤是可恢復的,而其他錯誤則發生在系統深處,已經超出了應用程式可以修正的範圍。

IO庫條件狀態:
在這裡插入圖片描述
在這裡插入圖片描述

while (cin >> word)
    // ok: 讀操作成功....

badbit 表示系統級錯誤,如不可恢復的讀寫錯誤。通常情況下,一旦 badbit 被置位,流就無法繼續使用了。在發生可恢復錯誤後,failbit 會被置位,如期望讀取數值卻讀出一個字元。如果到達檔案結束位置,eofbitfailbit 都會被置位。如果流未發生錯誤,則 goodbit 的值為0。如果 badbitfailbiteofbit 任何一個被置位,檢測流狀態的條件都會失敗。

good 函式在所有錯誤均未置位時返回 true。而 badfaileof 函式在對應錯誤位被置位時返回 true。此外,在 badbit 被置位時,fail 函式也會返回 true。因此應該使用 goodfail 函式確定流的總體狀態,eofbad 只能檢測特定錯誤。

流物件的 rdstate 成員返回一個 iostate 值,表示流的當前狀態。setstate 成員用於將指定條件置位(疊加原始流狀態)。clear 成員的無參版本清除所有錯誤標誌;含參版本接受一個 iostate 值,用於設定流的新狀態(覆蓋原始流狀態)。

// 記住cin的當前狀態
auto old_state = cin.rdstate();     // 記住cin的當前狀態
cin.clear();    			// 使cin有效
process_input(cin);     	// 使用cin
cin.setstate(old_state);    // 將cin置為原有狀態

3)管理輸出緩衝

每個輸出流都管理一個緩衝區,用於儲存程式讀寫的資料。

導致緩衝重新整理(即資料真正寫入輸出裝置或檔案)的原因有很多:

  • 程式正常結束。
  • 緩衝區已滿。
  • 使用操縱符(如 endl)顯式重新整理緩衝區。
  • 在每個輸出操作之後,可以用 unitbuf 操縱符設定流的內部狀態,從而清空緩衝區。預設情況下,對 cerr 是設定 unitbuf 的,因此寫到 cerr 的內容都是立即重新整理的。
  • 一個輸出流可以被關聯到另一個流。這種情況下,當讀寫被關聯的流時,關聯到的流的緩衝區會被重新整理。預設情況下,cincerr 都關聯到 cout,因此,讀 cin 或寫 cerr 都會重新整理 cout 的緩衝區。

flush 操縱符重新整理緩衝區,但不輸出任何額外字元。ends 向緩衝區插入一個空字元,然後重新整理緩衝區。

cout << "hi!" << endl;   // 輸出hi和一個換行,然後重新整理緩衝區
cout << "hi!" << flush;  // 輸出hi,然後重新整理緩衝區,不附加任何額外字元
cout << "hi!" << ends;   // 輸出hi和一個空字元,然後重新整理緩衝區

如果想在每次輸出操作後都重新整理緩衝區,可以使用 unitbuf 操縱符。它令流在接下來的每次寫操作後都進行一次 flush 操作。而 nounitbuf 操縱符則使流恢復使用正常的緩衝區重新整理機制。

cout << unitbuf;    // 所有輸出操作後都會立即重新整理緩衝區
// 任何輸出都立即重新整理,無緩衝
cout << nounitbuf;  // 回到正常的緩衝方式

如果程式異常終止,輸出緩衝區是不會被重新整理的。當一個程式崩潰後, 它所輸出的資料很可能停留在輸出緩衝區中等待列印。

當除錯一個已經崩潰的程式時,需要確認那些你認為已經輸出的資料確實已經重新整理了。否則, 可能將大量時間浪費在追蹤程式碼為什麼沒有執行上,而實際上程式碼已經執行了,只是程式崩潰後緩衝區沒有被重新整理,輸出資料被掛起沒有列印而已。

當一個輸入流被關聯到一個輸出流時,任何試圖從輸入流讀取資料的操作都會先重新整理關聯的輸出流。標準庫將coutcin關聯在一起,因此下面的語句會導致cout的緩衝區被重新整理:

cin >> ival;

互動式系統通常應該關聯輸入流和輸出流。這意味著包括使用者提示資訊在內的所有輸出,都會在讀操作之前被列印出來。

使用 tie 函式可以關聯兩個流。它有兩個過載版本:無參版本返回指向輸出流的指標。如果本物件已關聯到一個輸出流,則返回的就是指向這個流的指標,否則返回空指標。tie 的第二個版本接受一個指向 ostream 的指標,將本物件關聯到此 ostream

cin.tie(&cout);     // 僅僅是用來展示:標準庫將cin和cout關聯在一起
// old tie指向當前關聯到cin的流(如果有的話)
ostream *old_tie = cin.tie(nullptr); // cin不再與其他流關聯
// 將cin與cerr關聯;這不是一個好主意,因為cin應該關聯到cout
cin.tie(&cerr);     // 讀取cin會重新整理 cerr而不是cout
cin.tie(old_tie);   // 重建cin和cout間的正常關聯

每個流同時最多關聯一個流,但多個流可以同時關聯同一個 ostream。向 tie 傳遞空指標可以解開流的關聯。

2、檔案輸入輸出

標頭檔案 fstream 定義了三個型別來支援檔案IO:ifstream 從給定檔案讀取資料,ofstream 向指定檔案寫入資料,fstream 可以同時讀寫指定檔案。
在這裡插入圖片描述

1)使用檔案流物件

每個檔案流型別都定義了open函式,它完成一些系統操作,定位指定檔案,並視情況開啟為讀或寫模式。

建立檔案流物件時,如果提供了檔名(可選),open 會被自動呼叫。

ifstream in(ifile);   	// 構造一個ifstream並開啟給定檔案
ofstream out;   		// 輸出檔案流未關聯到任何檔案

在C++11中,檔案流物件的檔名可以是 string 物件或C風格字元陣列。舊版本的標準庫只支援C風格字元陣列。

在要求使用基類物件的地方,可以用繼承型別的物件代替。因此一個接受 iostream 型別引用或指標引數的函式,可以用對應的 fstream 型別來呼叫。

可以先定義空檔案流物件,再呼叫 open 函式將其與指定檔案關聯。如果 open 呼叫失敗,failbit 會被置位。

一旦一個檔案流已經開啟,它就保持與對應檔案的關聯。對一個已經開啟的檔案流呼叫 open 會失敗,並導致 failbit 被置位。隨後試圖使用檔案流的操作都會失敗。如果想將檔案流關聯到另一個檔案,必須先呼叫 close 關閉當前檔案,再呼叫 clear 重置流的條件狀態(close 不會重置流的條件狀態)。

fstream 物件被銷燬時,close 會自動被呼叫。

2)檔案模式

每個流都有一個關聯的檔案模式,用來指出如何使用檔案。
在這裡插入圖片描述
指定檔案模式有如下限制:

  • 只能對 ofstreamfstream 物件設定 out 模式。
  • 只能對 ifstreamfstream 物件設定 in 模式。
  • 只有當 out 被設定時才能設定 trunc 模式。
  • 只要 trunc 沒被設定,就能設定 app 模式。在 app 模式下,即使沒有設定 out 模式,檔案也是以輸出方式開啟。
  • 預設情況下,即使沒有設定 trunc,以 out 模式開啟的檔案也會被截斷。如果想保留以 out 模式開啟的檔案內容,就必須同時設定 app 模式,這會將資料追加寫到檔案末尾;或者同時設定 in 模式,即同時進行讀寫操作。
  • atebinary 模式可用於任何型別的檔案流物件,並可以和其他任何模式組合使用。
  • ifstream 物件關聯的檔案預設以 in 模式開啟,與 ofstream 物件關聯的檔案預設以 out 模式開啟,與 fstream 物件關聯的檔案預設以 inout 模式開啟。

每個檔案流型別都定義了一個預設的檔案模式,當我們未指定檔案模式時,就使用此預設模式。與 ifstream 關聯的檔案預設以 in 模式開啟;與 ofstream 關聯的檔案預設以 out 模式開啟;與 fstream 關聯的檔案預設以 inout 模式開啟。

預設情況下,開啟 ofstream 物件時,檔案內容會被丟棄,阻止一個 ofstream 清空給定檔案內容的方法是同時指定 app 模式:

流物件每次開啟檔案時都可以改變其檔案模式。

ofstream out;   			// 未指定檔案開啟模式
out.open("scratchpad");    	// 模式隱含設定為輸出和截斷
out.close();    			// 關閉out,以便我們將其用於其他檔案
out.open("precious", ofstream::app);   // 模式為out和app
out.close();

保留被 ofstream 開啟的檔案中已有資料的唯一方法是顯式指定 appin 模式。

3、string流

標頭檔案 sstream 定義了三個型別來支援記憶體IO:istringstreamstring 讀取資料,ostringstreamstring 寫入資料,stringstream 可以同時讀寫 string 的資料。
在這裡插入圖片描述

1)使用istringstream

當某些工作是對整行文字進行處理, 而其他一些工作是處理行內的單個單詞時,通常可以使用 istringstream

// 成員預設為公有
struct PersonInfo
{
    string name;
    vector<string> phones;
};

string line, word;   // 分別儲存來自輸入的一行和單詞
vector<PersonInfo> people;    // 儲存來自輸入的所有記錄
// 逐行從輸入讀取資料,直至cin遇到檔案尾(或其他錯誤)
while (getline(cin, line))
{
    PersonInfo info;    			// 建立一個儲存此記錄資料的物件
    istringstream record(line);    	// 將記錄繫結到剛讀入的行
    record >> info.name;    		// 讀取名字
    while (record >> word)  		// 讀取電話號碼
        info.phones.push_back(word);   // 保持它們
    people.push_back(info);    		// 將此記錄追加到people末尾
}

2)使用ostringstream

當逐步構造輸出, 希望最後一起列印時, ostringstream 是很有用的。

for (const auto &entry : people)
{ 
	// 對people中每一項
    ostringstream formatted, badNums;   	// 每個迴圈步建立的物件
    for (const auto &nums : entry.phones)
    { 
    	// 對每個數
        if (!valid(nums))
        {
            badNums << " " << nums;  		// 將數的字串形式存入badNums
        }
        else
            // 將格式化的字串"寫入"
            formatted << " " << format(nums);
    }
    if (badNums.str().empty())   			// 沒有錯誤的數
        os << entry.name << " "  			// 列印名字
            << formatted.str() << endl;   	// 和格式化的數
    else  // 否則,列印名字和錯誤的數
        cerr << "input error: " << entry.name
            << " invalid number(s) " << badNums.str() << endl;
}

參考文章

  • 《C++ Primer》

相關文章