《C++ Primer》學習筆記(八):IO庫
IO類
頭文件iostream
定義了用於讀寫流的基本類型,fstream
定義了讀寫命名文件的類型,sstream
定義了讀寫內存中 string
對象的類型。
爲了支持使用寬字符的語言,標準庫定義了一組類型和對象來操縱wchar_t
類型的數據。寬字符版本的類型和函數的名字以一個w
開始。
標準庫使用繼承機制來忽略不同類型的流之間的差異。例如類型ifstream
和istringstream
都繼承自istream
。因此可以像使用istream
對象一樣來使用ifstream
和istringstream
。
IO象無拷貝或賦值
ofstream out1, out2;
out1 = out2; // 錯誤:不能對流對象賦值
ofstream print(ofstream); // 錯誤:不能初始化ofstream參數
out2 = print(out2); // 錯誤:不能拷貝流對象
由於不能拷貝IO對象,並且讀寫一個IO對象會改變其狀態,因此一般以非const
引用的方式傳遞和返回流。
條件狀態
IO操作一個與生俱來的問題就是可能發生錯誤。於是IO類定義了一些函數和標誌以幫助用戶訪問和操縱流的條件狀態。
一個流一旦發生錯誤,其上的後續IO操作都會失敗。確定一個流對象的狀態的最簡單方法是將它當作一個條件來使用:
while(cin >> word)
//ok:讀操作成果...
- IO庫定義了一個與機器無關的
iostate
類型,提供了表達流狀態的完整功能,可以與位運算符一起使用來一次性檢測或設置多個標誌位。 badbit
表示系統級錯誤,一旦badbit
被置位,流就無法再使用了。- 在發生可恢復錯誤時,
failbit
被置位,這種問題通常可以修正,修正後流可以繼續使用。 - 當流沒有任何錯誤時,
goodbit
的值位0. - 如果到達文件結束位置,
eofbit
和failbit
都會被置位。 - 如果
badbit
、failbit
和eofbit
任一個被置位,則檢測流狀態的條件會失敗。
管理條件狀態
- 流對象的
rdstate
成員返回一個iostate
值,對應流的當前狀態。 clear
不接受參數的版本清除(復位)所有錯誤標誌位。clear
的帶參數版本接受一個iostate
值,表示流的新狀態。
// 記住cin的當前狀態
auto old_state = cin.rdstate(); // 記住cin的當前狀態
cin.clear(); // 使cin有效
process_input(cin); // 使用cin
cin.setstate(old_state); // 將cin置爲原有狀態
//復位failbit和badbit,其他標誌位保持不變
cin.clear(cin.rdstate() & ~cin.failbit & ~cin.badbit);
管理輸出緩衝
由於設備的寫操作可能很耗時,允許操作系統將多個輸出操作組合爲單一的設備寫操作可以帶來很大的性能提升。
導致緩衝刷新(即數據真正寫到輸出設備或者文件)的原因有很多:
- 程序正常結束
- 緩衝區滿
- 使用操作符
endl
來顯式刷新緩衝區 - 在每個輸出操作之後,可以用
unitbuf
操縱符設置流的內部狀態,從而清空緩衝區。默認情況下,對cerr
是設置unitbuf
的,因此寫到cerr
的內容都是立即刷新的。 - 一個輸出流可以被關聯到另一個流。這種情況下,當讀寫被關聯的流時,關聯到的流的緩衝區會被刷新。默認情況下,
cin
和cerr
都關聯到cout
,因此,讀cin
或寫cerr
都會刷新cout
的緩衝區。
cout << "hi!" << endl; // 輸出hi和一個換行,然後刷新緩衝區
cout << "hi!" << flush; // 輸出hi,然後刷新緩衝區,不附加任何額外字符
cout << "hi!" << ends; // 輸出hi和一個空字符,然後刷新緩衝區
如果想在每次輸出操作後都刷新緩衝區,可以使用 unitbuf
操縱符。它令流在接下來的每次寫操作後都進行一次flush
操作。而 nounitbuf
操縱符則使流恢復使用正常的緩衝區刷新機制。
cout << unitbuf; // 所有輸出操作後都會立即刷新緩衝區
// 任何輸出都立即刷新,無緩衝
cout << nounitbuf; // 回到正常的緩衝方式
如果程序異常終止,輸出緩衝區是不會被刷新的。當一個程序崩潰後, 它所輸出的數據很可能停留在輸出緩衝區中等待打印。
當調試一個已經崩潰的程序時,需要確認那些你認爲已經輸出的數據確實已經刷新了。否則, 可能將大量時間浪費在追蹤代碼爲什麼沒有執行上,而實際上代碼已經執行了,只是程序崩潰後緩衝區沒有被刷新,輸出數據被掛起沒有打印而已。
當一個輸入流被關聯到一個輸出流時,任何試圖從輸入流讀取數據的操作都會先刷新關聯的輸出流。標準庫將cout
和cin
關聯在一起,因此下面的語句會導致cout
的緩衝區被刷新:
cin >> ival;
使用 tie
函數可以關聯兩個流。它有兩個重載版本:無參版本返回指向輸出流的指針。如果本對象已關聯到一個輸出流,則返回的就是指向這個流的指針,否則返回空指針。tie
的第二個版本接受一個指向 ostream
的指針,將本對象關聯到此 ostream
。交互式系統通常應該關聯輸入流和輸出流。這意味着包括用戶提示信息在內的所有輸出,都會在讀操作之前被打印出來。每個流同時最多關聯一個流,但多個流可以同時關聯同一個 ostream
。向 tie 傳遞空指針nullptr
可以解開流的關聯。
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間的正常關聯
文件輸入輸出
除了繼承自iostream
類型的行爲外,fstream
中定義的類型還增加了一些新的成員來管理與流相關聯的文件。
使用文件流對象
創建文件流對象時如果提供文件名作爲參數,則open
會自動被調用。
ifstream in(ifile); // 構造一個ifstream並打開給定文件
ofstream out; // 輸出文件流未關聯到任何文件
可以先定義空文件流對象,再調用 open
函數將其與指定文件關聯。如果 open
調用失敗,failbit
會被置位。進行open
是否成功的檢測通常是一個好習慣:
if(out)
//open成功了,可以使用文件了
當一個fstream
對象被銷燬時,close
會自動被調用。
文件模式
- 只能對
ofstream
或fstream
對象設定out
模式。 - 只能對
ifstream
或fstream
對象設定in
模式。 - 只有當
out
被設定時才能設定trunc
模式。 - 只要
trunc
沒被設定,就能設定app
模式。在app
模式下,即使沒有設定out
模式,文件也是以輸出方式打開。 - 默認情況下,即使沒有設定
trunc
,以out
模式打開的文件也會被截斷。如果想保留以out
模式打開的文件內容,就必須同時設定app
模式,這會將數據追加寫到文件末尾;或者同時設定 in 模式,即同時進行讀寫操作。 ate
和binary
模式可用於任何類型的文件流對象,並可以和其他任何模式組合使用。- 與
ifstream
對象關聯的文件默認以in
模式打開,與ofstream
對象關聯的文件默認以out
模式打開,與fstream
對象關聯的文件默認以in
和out
模式打開。
保留被ofstream
打開的文件中已有數據的唯一方法是顯式指定app
或in
模式。
ofstream out; // 未指定文件打開模式
out.open("scratchpad"); // 模式隱含設置爲輸出和截斷
out.close(); // 關閉out,以便我們將其用於其他文件
out.open("precious", ofstream::app); // 模式爲out和app
out.close();
string流
除了繼承得來的操作,sstream
中定義的類型還增加了一些成員來管理與流相關聯的string
。
使用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末尾
}
使用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;
}
練習
- 編寫函數,接受一個
istream&
參數,返回值類型也是istream&
。此函數須從給定流中讀取數據,直至遇到文件結束標識時停止。它將讀取的數據打印在標準輸出上。完成這些操作後,在返回流之前,對流進行復位,使其處於有效狀態。
#include<iostream>
#include<stdexcept>
using namespace std;
istream& func(istream & in){
int n;
while (in >> n, !in.eof()) // 直到遇到文件結束符才停止讀取
{
if (in.bad())
throw runtime_error("IO stream error");
if (in.fail()){
cerr << "data error, please try again!" << endl;
in.clear();
in.ignore(100, '\n');
continue;
}
cout << n << endl;
}
in.clear();
return in;
}
int main()
{
cout << "Please enter some integers and press Ctrl+Z to finish" << endl;
func(cin);
system("pause");
return 0;
}
- 什麼情況下,下面的
while
循環會終止?
while (cin >> i) /* ... */
- 遇到了文件結束符
- IO流錯誤
- 讀入了無效數據
- 編寫函數,以讀模式打開一個文件,將其內容讀入到一個
string
的vector
中,將每一行作爲一個獨立的元素存於vector
中。
#include<iostream>
#include <fstream>
#include <string>
#include <vector>
using namespace std;
int main()
{
ifstream file("test.txt");
if(!file){
cerr << "can't open input file" << endl;
system("pause");
return -1;
}
string line;
vector<string> vs;
while(getline(file, line)){
vs.push_back(line);
}
file.close();
for(const auto& item: vs){
cout << item << endl;
}
system("pause");
return 0;
}