文章目錄
- 前言
- IO頭文件
- iostream
- fstream
- sstream
- 流的使用
- 不能拷貝或對 IO對象 賦值
- 條件狀態與 iostate 類型
- 輸出緩沖區
- 文件流
- fstream類型
- 文件模式
- 文件光標函數
- tellg() / tellp()
- seekg() / seekp()
- 向文件存儲內容/讀取文件內容
- string流
- istringstream
- ostringstream
前言
我們在使用 C++
的過程中,總避免不了 IO操作
,比如經常用到的一些 IO庫設施
:
istream
:(輸入流)類型,提供輸入操作。ostream
:(輸出流)類型,提供輸出操作。cin
:一個istream
對象,從標準輸入讀取數據。cout
:一個ostream
對象,向標準輸出寫入數據。cerr
:一個ostream
對象,通常用于輸出程序錯誤消息,寫入到標準錯誤。>>運算符
:用來從一個istream
對象讀取輸入數據。<<運算符
:用來向一個ostream
對象寫入輸出數據。getline函數
:從一個給定的istream
讀取一行數據,存入一個給定的string
對象中。
但實際上可能僅僅是懵懵懂懂在使用,如果不深入了解的話,這樣的使用是淺薄的。
IO頭文件
iostream
定義了用于讀寫流的基本類型。
- istream,wistream 從流讀取數據
- ostream,wostream 向流寫入數據
- iostream,wiostream 讀寫流
fstream
定義了讀寫命名文件的類型。
- ifstream,wifstream 從文件讀取數據
- ofstream,wofstream 向文件寫入數據
- fstream,wfstream 讀寫文件
sstream
定義了讀寫內存string對象的類型。
- istringstream,wistringstream 從 string 讀取數據
- ostringstream,wostringstream 向 string 寫入數據
- stringstream,wstringstream 讀寫 string
流的使用
標準庫通過繼承使我們忽略不同類型流之間的差異。舉例來說,類型 ifstream
和 istringstream
都繼承自 istream
。因此,我們如何使用 cin
,就可以同樣地使用這些類型的對象。
不能拷貝或對 IO對象 賦值
ofstream out1, out2;
out1 = out2; // error:不能對流對象賦值
ofstream printf(ofstream); // error: 不能初始化ofstream參數
out2 = printf(out2); // error: 不能拷貝流對象
- 由于不能拷貝IO對象,因此我們也不能將形參或返回類型設置為流類型。
- 進行IO操作的函數通常以引用方式傳遞和返回流。讀寫一個IO對象會改變其狀態,因此傳遞和返回的引用不能是const的。
條件狀態與 iostate 類型
IO操作使用不當的話會發生錯誤,而如果是發生在系統深處的錯誤,那么就超出了應用程序可以修正的范圍。但也有一些錯誤是可以恢復的,IO類也提供了一些函數和標志來訪問、操縱流的條件狀態。
下面對表中的四個條件位作進一步介紹。
iostate 類型
IO庫定義了一個與機器無關的 iostate
類型,它提供了表達流狀態功能。
IO庫定義了 4個 iostate類型
的 constexpr
值(常量表達式),表示特定的位模式:
- badbit: 表示系統級錯誤,如不可恢復的讀寫錯誤。通常情況下,一旦
badbit
被置位,流就無法再使用了。 - failbit: 在發生可恢復錯誤后被置位,如期望讀取數值卻讀出一個字符等錯誤。這種問題通常是可以修正的,流還可以繼續使用。
- eofbit: 如果到達文件結束位置,連同
failbit
一起被置位。 - goodbit: 值為
0
時,表示流未發生錯誤。
對他們進行一個簡單的使用:
auto old_state = cin.rdstate(); // 返回流cin的當前狀態,返回值類型為 strm::iostate
cin.clear(); // 將cin所有條件位復位,換言之,使cin有效
// clear重載版本允許有參數,接受一個iostate值,表示流的新狀態
cin.clear(cin.rdstate() & ~cin.failbit & ~cin.badbit);
// 先用rdstate讀出當前條件狀態,再將failbit和badbit復位生成新狀態。
process_input(cin); // 使用cin
cin.setstate(old_state); // 將cin置為原有狀態
輸出緩沖區
以前對于輸出緩沖區是沒什么概念的……直到在做美團往年筆試題的時候,有道編程題如果用 endl
作為換行會報超時,原因是 endl
頻繁刷新輸出緩沖區,因此需要用 '\n'
。
操作系統的 IO操作
是很耗時的,緩沖機制使操作系統將程序的多個輸出操作組合成單一的系統級寫操作(寫到顯示設備上),對性能的提升是巨大的。
導致緩沖刷新(數據真正寫到輸出設備或文件)的原因有很多:
- 程序正常結束,作為
main函數
的return操作
的一部分,緩沖刷新被執行。 - 緩沖區滿時,需要刷新緩沖,而后新的數據才能繼續寫入緩沖區。
- 我們可以使用操縱符(如
endl
) 來顯式刷新緩沖區。 - 在每個輸出操作之后,我們可以用
操縱符unitbuf
設置流的內部狀態,來清空緩沖區。默認情況下,對cerr
是設置unitbuf
的,因此寫到cerr
的內容都是立即刷新的。 - 一個輸出流可能被關聯到另一個流。在這種情況下,當讀寫被關聯的流時,關聯到的流的緩沖區會被刷新。例如,默認情況下,
cin
和cerr
都關聯到cout
。因此,讀cin
或寫cerr
都會導致cout
的緩沖區被刷新。
關于第三點,共有三種操作符可用來刷新緩沖:
- endl: 換行并刷新緩沖區
- flush: 僅刷新緩沖區,但不輸出任何額外字符
- ends: 向緩沖區插入一個空字符然后刷新緩沖區
關于第四點:
- unitbuf操縱符: 每次寫操作之后都進行一次 flush 操作
- nounitbuf操縱符: 重置流,使其恢復默認的緩沖區刷新機制
值得一提的是,如果程序崩潰,輸出緩沖區不會被刷新。
關于第五點,C++提供了 tie函數
來查看當前對象關聯的輸入輸出流,tie
有兩個重載版本:
- 無參數版本: 返回指向輸出流的指針。當前對象若關聯了一個輸出流,則返回指向該流的指針;若未關聯流,則返回空指針。
- 參數為一個指向 ostream 的指針: 將當前對象關聯到此
ostream
。
每個流同時最多關聯到一個流,但多個流可以同時關聯到同一個 ostream
。
示例:
cin.tie(&cout); // 標準庫默認將 cin 和 cout 關聯在一起
cin.tie() = cin.tie(nullptr); // 通過傳遞空指針,讓 cin 不再與其他流關聯
文件流
fstream類型
除了繼承自 iostream類型
的行為外,fstream類型
還增加了一些新的成員來管理與流關聯的文件。
open函數
fstrm(s)
之所以能在調用時打開文件s,是因為自動調用了 open函數
,等價于:
ifstream in; // 輸入文件流未與任何文件關聯
in.open(ifile); // 打開指定文件,并與in綁定
對一個已經打開的文件流調用 open
會失敗,此時 failbit
會被置位,隨后使用文件流的操作都會失敗。因此,調用 open
后檢測是否成功是個好習慣:
if(in) // 成功
else // 不成功
如果想要將文件流關聯到另一個文件,必須先關閉已關聯的文件:
in.cloes();
in.open(ifile);
open
成功調用會將 good()
設為 true
。
close函數
當一個 fstream對象
被銷毀時,close
會自動被調用。
文件模式
每個文件流類型都定義了一個默認的文件模式,當我們未指定文件模式時,就使用此默認模式。
- 與
ifstream關聯
的文件默認以in模式
打開; - 與
ofstream關聯
的文件默認以out模式
打開; - 與
fstream關聯
的文件默認以in和out模式
打開。
雖然不論是調用 open
打開文件,還是 fstrm(s)
這樣隱式打開文件,都可以指定文件模式,但指定文件模式有如下限制:
- 只可以對
ofstream
、fstream
對象設定out
模式。 - 只可以對
ifstream
、fstream
對象設定in
模式。 - 只有
out
也被設定時才可設定trunc
模式。 - 只要
trunc
沒被設定,就可以設定app
模式。在app
模式下,即使沒有顯式指定out
模式,文件也總是以輸出方式被打開。 - 默認情況下,以
out
模式打開的文件同時使用trunc
模式,即會被截斷(內容被丟棄)。- 為了保留以
out
模式打開的文件的內容,我們必須同時指定app
模式,這樣只會將數據追加寫到文件末尾; - 或者同時指定
in
模式,即打開文件同時進行讀寫操作。
- 為了保留以
ate
和binary
模式可用于任何類型的文件流對象,且可以與其他任何文件模式組合使用。
關于第五點,舉例詳細講一下:
/*截斷*/
ofstream out1("file1"); // 隱含以out模式打開文件并截斷文件
ofstream out2("file1", ofstream::out); // 隱含地截斷文件
ofstream out3("file1", ofstream::out | ofstream::trunc);
// 顯式實現out模式打開文件并截斷/*app模式保留文件內容*/
ofstream app1("file2", ofstream:app); // 隱含out模式
ofstream app2("file2", ofstream:out | ofstream:app);
文件光標函數
tellg() / tellp()
該函數沒有參數,返回 pos_type
類型的值,就是一個整數,代表當前 讀取光標【tellg】 / 寫入光標【tellp】 的位置距文件首的字節數。
seekg() / seekp()
g
表示get
,指示函數在輸入流上工作。該函數的作用移動讀操作光標。p
是put
縮寫,指示函數在輸出流上工作。seekp
用于移動寫操作光標。
// 一個參數
basic_istream<Elem, Tr>& seekg( pos_type pos
);// 兩個參數
basic_istream<Elem, Tr>& seekg( off_type off, ios_base::seekdir way
);// seekp 函數原型及參數信息同 seekg
pos
:移動讀取指針的絕對位置。要求傳入的參數類型與函數tellg
(見下文)的返回值類型相同。off
:偏移量,單位字節(B
)。正數表示向右偏移,負數表示向左偏移。way
:基地址,off
根據該地址進行偏移。有下面三個取值:
描述 | 模式標志 |
---|---|
文件首 | std::ios::beg |
文件尾 | std::ios::end |
當前光標位置 | std::ios::cur |
注意,如果目前已經在文件末尾,則在調用seekg
之前,必須清除文件末尾的標志:
fstream ioFile("文件路徑");ioFile.get(ch); // 先將字符讀入流
while (!ioFile.fail())
{cout.put(ch); // 再將流中內容輸出到屏幕ioFile.get(ch); //
}
// 假設此時已經讀取到文件流對象的末尾
/* 缺少調用 clear() */
文件流對象.seekg(0L, ios::beg); // 移動到文件開頭
文件流對象.tellg(); // 返回-1,說明上一步并為真正移動到文件開頭// 正確做法
文件流對象.clear();
文件流對象.seekg(0L, ios::beg); // 移動到文件開頭
向文件存儲內容/讀取文件內容
// url 文件路徑
void write(std::string &url){std::ofstream fwrite(url, std::ios::binary);if (!fwrite.is_open()) {std::cout << "open url fail" << std::endl;}/* 寫入數據 */// 內置類型int i = 1024;fwrite.write((const char *) &i, sizeof(i));// 數組std::vector<std::string> stringVec = {"a", "b", "c"};int64_t vecTotalSize = sizeof(std::string) * stringVec.size();fwrite.write((const char *) stringVec.data(), vecTotalSize);// 寫入數組數據時需要記錄總數據的長度fwrite.write((const char *) &vecTotalSize, sizeof(vecTotalSize));// 自定義結構People people;people.name = "lihua";people.age = 21;fwrite.write((const char *) &people, sizeof(people));// 關閉流fwrite.close();
}
void read(std::string &url){std::ifstream fread;fread.open(url, std::ofstream::binary);if (!fread.is_open()) {std::cout << "open url fail" << std::endl;}/* 讀取數據,順序和寫入順序相反 */// 自定義結構People people;int peopleLen = sizeof(people);fread.seekg(-peopleLen, std::ios::end);fread.read((char *) &people, peopleLen);// 數組int64_t vecSize; // 先讀取數組數據的長度const int vecSizeLen = sizeof(vecSize);// std::ios::cur 在 People 數據的結尾處// 讀取 vecSize 需要將指針左移到 vecSize 數據的開頭// 這就需要經過 peopleLen 和 vecSizeLen 兩個長度fread.seekg(-peopleLen - vecSizeLen, std::ios::cur);fread.read((char *) &vecSize, vecSizeLen); //8 bytesint64_t stringVecSize = vecSize / sizeof(std::string); // 根據數組數據的長度算出數組的大小std::vector<std::string> stringVec;stringVec.resize(stringVecSize);fread.seekg(-vecSizeLen - vecSize, std::ios::cur);auto pos = fread.tellg();fread.read((char *) stringVec.data(), vecSize);pos = fread.tellg();// 內置類型int j;int len = sizeof(j);fread.seekg(-vecSize - len, std::ios::cur);fread.read((char*) &j, len);// 關閉流fread.close();
}
string流
同樣的,除了繼承自 iostream
的操作,sstream
也增加了獨有的操作。
istringstream
我們經常會碰到處理整行字符串的問題,比如:比較版本號
用雙指針截取字符串當然是一種方法,但是使用 istringstream
這個標準庫提供的利器會更加方便。當然,兩種方法的時間、空間復雜度是一樣的。
下面通過分析 istringstream
的使用來進一步理解如何用:
class Solution {
public:int compareVersion(string version1, string version2) {istringstream in1(version1); // 將文本version1與輸入流in1綁定istringstream in2(version2);int a, b;char c;while(in1.good() || in2.good()){in1 >> a; // 從in1中讀取int數據到a中,遇到空白符or非int數據停下in2 >> b;if(a > b) return 1;if(a < b) return -1;a = b = 0;in1 >> c; // 從in1中讀取char類型數據到c中,遇到空白符or非char數據停下in2 >> c;}return 0;}
};
再比如,有這樣的輸入,人名和他們的常用密碼,一個人可能有多種常用密碼:
cmy 12345 22345
lx 6644
lhy 6633 1221 5665
那么我們可以這樣處理:
struct per_pw{string name;vector<string> pw;
}
string s, word; // s暫存來自輸入的一行文本
vector<per_pw> people;
while(getline(cin, s)){ // 處理一行文本,也就是一個人的信息per_pw pp;istringstream in(s); // 將in綁定到剛讀取的sin >> pp.name; // 讀取名字while(in >> word) // 讀取密碼pp.pw.push_back(word); // 密碼存入pp的pw數組中people.push_back(pp); // 將這個人的信息保存在people數組中
}
ostringstream
當我們希望將多個輸出最后一起打印時,ostringstream
是很有用的。舉個簡單的例子:
ostringstream out; // 創建一個未綁定的輸出流
vector<string> vs = {"cmy", "lx", "lhy"};
for (string s : vs) {out << s << " ";
}
cout << out.str() << endl;
// str():返回out保存的string的拷貝,也就是將out轉換為string類型。
我們使用標準的輸出運算符<<向 out
寫入數據,有趣的是,這些寫入操作實際上轉換為 string
操作,向 out
中的 string
對象添加字符。