IOstream 標準庫

C++ 的輸入/輸出(input/output)由標準庫提供。
標準庫定義了一族類型,支持對文件和控制窗口等設備的讀寫(IO)。
還定義了其他一些類型,使 string對象能夠像文件一樣操作,從而使我們無須 IO 就能實現數據與字符之間的轉換。
這些 IO 類型都定義瞭如何讀寫內置數據類型的值。

前面的程序已經使用了多種 IO 標準庫提供的工具:
? istream(輸入流)類型,提供輸入操作。
? ostream(輸出流)類型,提供輸出操作。
? cin(發音爲 see-in):讀入標準輸入的 istream 對象。
? cout(發音爲 see-out):寫到標準輸出的 ostream 對象。
? cerr(發音爲 see-err):輸出標準錯誤的 ostream 對象。cerr 常用於程序錯誤信息。
? >> 操作符,用於從 istream 對象中讀入輸入。
? << 操作符,用於把輸出寫到 ostream 對象中。
? getline 函數,需要分別取 istream 類型和 string 類型的兩個引用形參,
其功能是從 istream 對象讀取一個單詞,然後寫入 string 對象中。

8.1. 面向對象的標準庫
如果兩種類型存在繼承關係,則可以說一個類“繼承”了其父類的行爲——接口。
C++ 中所提及的父類稱爲基類(base class),而繼承而來的類則稱爲派生類(derived class)

IO 類型在三個獨立的頭文件中定義:
iostream 定義讀寫控制窗口的類型,
fstream 定義讀寫已命名文件的類型,
sstream 所定義的類型則用於讀寫存儲在內存中的 string 對象。
在 fstream 和 sstream 裏定義的每種類型都是從iostream 頭文件中定義的相關類型派生而來。

表 8.1. IO 標準庫類型和頭文件

  1. 國際字符的支持
    迄今爲止,所描述的流類(stream class)讀寫的是由 char 類型組成的流。
    此外,標準庫還定義了一組相關的類型,支持 wchar_t 類型。
    每個類都加上“w”前綴,以此與 char 類型的版本區分開來。

  2. IO 對象不可複製或賦值
    出於某些原因,標準庫類型不允許做複製或賦值操作

ofstream out1, out2;
out1 = out2; // error: cannot assign stream objects

// print function: parameter is copied
ofstream print(ofstream);
out2 = print(out2); // error: cannot copy stream objects

8.2. 條件狀態
表 8.2. IO 標準庫的條件狀態

  1. 流狀態的查詢和控制
    可以如下管理輸入操作

int ival;

// read cin and test only for EOF; loop is executed even if there are other IO failures
while (cin >> ival, !cin.eof()) {
if (cin.bad()) // input stream is corrupted; bail out
throw runtime_error(“IO stream corrupted”);

if (cin.fail()) { // bad input
cerr<< “bad data, try again”; // warn the user
cin.clear(istream::failbit); // reset the stream
continue; // get next input
}

// ok to process ival
}
這個循環不斷讀入 cin,直到到達文件結束符或者發生不可恢復的讀取錯誤爲止

  1. 條件狀態的訪問
    rdstate 成員函數返回一個 iostate 類型值,該值對應於流當前的整個條件狀態:

// remember current state of cin
istream::iostate old_state = cin.rdstate();
cin.clear();
process_input(); // use cin
cin.clear(old_state); // now reset cin to old state

  1. 多種狀態的處理
    常常會出現需要設置或清除多個狀態二進制位的情況。
    此時,可以通過多次調用 setstate 或者 clear 函數實現。
    另外一種方法則是使用按位或(OR)操作符在一次調用中生成“傳遞兩個或更多狀態位”的值。
    按位或操作使用其操作數的二進制位模式產生一個整型數值。
    對於結果中的每一個二進制位,如果其值爲 1,則該操作的兩個操作數中至少有一個的對應二進制位是 1。
    例如:

// sets both the badbit and the failbit
is.setstate(ifstream::badbit | ifstream::failbit);

將對象 is 的 failbit 和 badbit 位同時打開。實參:
is.badbit | is.failbit

生成了一個值,其對應於 badbit 和 failbit 的位都打開了,
也就是將這兩個位都設置爲 1,該值的其他位則都爲 0。
在調用 setstate 時,使用這個值來開啓流條件狀態成員中對應的 badbit 和 failbit 位

8.3. 輸出緩衝區的管理
每個 IO 對象管理一個緩衝區,用於存儲程序讀寫的數據。
如有下面語句:
os << “please enter a value: “;

系統將字符串字面值存儲在與流 os 關聯的緩衝區中。

下面幾種情況將導致緩衝區的內容被刷新,即寫入到真實的輸出設備或者文件:
1. 程序正常結束。作爲 main 返回工作的一部分,將清空所有輸出緩衝區。
2. 在一些不確定的時候,緩衝區可能已經滿了,在這種情況下,緩衝區將會在寫下一個值之前刷新。
3. 用操縱符顯式地刷新緩衝區,例如行結束符 endl。
4. 在每次輸出操作執行完後,用 unitbuf 操作符設置流的內部狀態,從而清空緩衝區。
5. 可將輸出流與輸入流關聯(tie)起來。在這種情況下,在讀輸入流時將刷新其關聯的輸出緩衝區。

  1. 輸出緩衝區的刷新
    我們的程序已經使用過 endl 操縱符,用於輸出一個換行符並刷新緩衝區。
    除此之外,C++ 語言還提供了另外兩個類似的操縱符。
    第一個經常使用的 flush,用於刷新流,但不在輸出中添加任何字符。
    第二個則是比較少用的 ends,這個操縱符在緩衝區中插入空字符 null,然後後刷新它:

cout << “hi!” << flush; // flushes the buffer; adds no data
cout << “hi!” << ends; // inserts a null, then flushes the buffer
cout << “hi!” << endl; // inserts a newline, then flushes the buffer

  1. unitbuf 操縱符
    如果需要刷新所有輸出,最好使用 unitbuf 操縱符。
    這個操縱符在每次執行完寫操作後都刷新流:

cout << unitbuf << “first” << ” second” << nounitbuf;
等價於:
cout << “first” << flush << ” second” << flush;

nounitbuf 操縱符將流恢復爲使用正常的、由系統管理的緩衝區刷新方式。

  1. 警告:如果程序崩潰了,則不會刷新緩衝區
    如果程序不正常結束,輸出緩衝區將不會刷新。
    在嘗試調試已崩潰的程序時,通常會根據最後的輸出找出程序發生錯誤的區域。
    如果崩潰出現在某個特定的輸出語句後面,則可知是在程序的這個位置之後出錯。
    調試程序時,必須保證期待寫入的每個輸出都確實被刷新了。
    因爲系統不會在程序崩潰時自動刷新緩衝區,這就可能出現這樣的情況:
    程序做了寫輸出的工作,但寫的內容並沒有顯示在標準輸出上,仍然存儲在輸出緩衝區中等待輸出。
    如果需要使用最後的輸出給程序錯誤定位,則必須確定所有要輸出的都已經輸出。
    爲了確保用戶看到程序實際上處理的所有輸出,最好的方法是保證所有的輸出操作都顯式地調用了 flush 或 endl。
    如果僅因爲緩衝區沒有刷新,程序員將浪費大量的時間跟蹤調試並沒有執行的代碼。
    基於這個原因,輸出時應多使用 endl 而非 ‘\n’。
    使用endl 則不必擔心程序崩潰時輸出是否懸而未決(即還留在緩衝區,未輸出到設備中)。

  2. 將輸入和輸出綁在一起
    當輸入流與輸出流綁在一起時,任何讀輸入流的嘗試都將首先刷新其輸出流關聯的緩衝區。
    標準庫將 cout 與 cin 綁在一起,因此語句:

cin >> ival;

導致 cout 關聯的緩衝區被刷新。

交互式系統通常應確保它們的輸入和輸出流是綁在一起的。
這樣做意味着可以保證任何輸出,包括給用戶的提示,都在試圖讀之前輸出。
tie 函數可用 istream 或 ostream 對象調用,使用一個指向 ostream 對象的指針形參。
調用 tie 函數時,將實參流綁在調用該函數的對象上。
如果一個流調用 tie 函數將其本身綁在傳遞給 tie 的 ostream 實參對象上,
則該流上的任何 IO 操作都會刷新實參所關聯的緩衝區。

cin.tie(&cout); // illustration only: the library ties cin and cout for us
ostream *old_tie = cin.tie();

cin.tie(0); // break tie to cout, cout no longer flushed when cin is read
cin.tie(&cerr); // ties cin and cerr, not necessarily a good idea!

// …
cin.tie(0); // break tie between cin and cerr
cin.tie(old_tie); // restablish normal tie between cin and cout

一個 ostream 對象每次只能與一個 istream 對象綁在一起。
如果在調用tie 函數時傳遞實參 0,則打破該流上已存在的捆綁。

8.4. 文件的輸入和輸出
fstream 頭文件定義了三種支持文件 IO 的類型:
1. ifstream,由 istream 派生而來,提供讀文件的功能。
2. ofstream,由 ostream 派生而來,提供寫文件的功能。
3. fstream,由 iostream 派生而來,提供讀寫同一個文件的功能。

這些類型都由相應的 iostream 類型派生而來,
這個事實意味着我們已經知道使用 fstream 類型需要了解的大部分內容了。
特別是,可使用 IO 操作符(<<和 >> )在文件上實現格式化的 IO,
而且在前面章節介紹的條件狀態也同樣適用於 fstream 對象。

fstream 類型除了繼承下來的行爲外,
還定義了兩個自己的新操作—— open和 close,以及形參爲要打開的文件名的構造函數。
fstream、ifstream 或ofstream 對象可調用這些操作,而其他的 IO 類型則不能調用。

8.4.1. 文件流對象的使用
迄今爲止,我們的程序已經使用過標準庫定義的對象:cin、cout 和 cerr。
需要讀寫文件時,則必須定義自己的對象,並將它們綁定在需要的文件上。
假設ifile 和 ofile 是存儲希望讀寫的文件名的 strings 對象,
可如下編寫代碼:

// construct an ifstream and bind it to the file named ifile
ifstream infile(ifile.c_str());

// ofstream output file object to write file named ofile
ofstream outfile(ofile.c_str());

上述代碼定義並打開了一對 fstream 對象。infile 是讀的流,而 outfile則是寫的流。
爲 ifstream 或者 ofstream 對象提供文件名作爲初始化式,就相當於打開了特定的文件。

ifstream infile; // unbound input file stream
ofstream outfile; // unbound output file stream

上述語句將 infile 定義爲讀文件的流對象,將 outfile 定義爲寫文件的對象。
這兩個對象都沒有捆綁具體的文件。在使用 fstream 對象之前,還必須使這些對象捆綁要讀寫的文件:

infile.open(“in”); // open file named “in” in the current directory
outfile.open(“out”); // open file named “out” in the current directory

調用 open 成員函數將已存在的 fstream 對象與特定文件綁定。
爲了實現讀寫,需要將指定的文件打開並定位,open 函數完成系統指定的所有需要的操作。

  1. 警告:C++ 中的文件名
    由於歷史原因,IO 標準庫使用 C 風格字符串而不是 C++ strings 類型的字符串作爲文件名。
    在創建 fstream 對象時,如果調用open 或使用文件名作初始化式,需要傳遞的實參應爲 C 風格字符串,
    而不是標準庫 strings 對象。程序常常從標準輸入獲得文件名。
    通常,比較好的方法是將文件名讀入 string 對象,而不是 C 風格字符數組。
    假設要使用的文件名保存在 string 對象中,則可調用 c_str 成員獲取 C 風格字符串。

  2. 檢查文件打開是否成功
    打開文件後,通常要檢驗打開是否成功,這是一個好習慣:

// check that the open succeeded
if (!infile) {
cerr << “error: unable to open input file: ” << ifile << endl;
return -1;
}

這個條件與之前測試 cin 是否到達文件尾或遇到某些其他錯誤的條件類似。
檢查流等效於檢查對象是否“適合”輸入或輸出。如果打開(open)失敗,
則說明 fstream 對象還沒有爲 IO 做好準備。

當測試對象
if (outfile) // ok to use outfile?

返回 true 意味着文件已經可以使用。由於希望知道文件是否未準備好,則對返回值取反來檢查流:

if (!outfile) // not ok to use outfile?

  1. 將文件流與新文件重新捆綁
    fstream 對象一旦打開,就保持與指定的文件相關聯。
    如果要把 fstream 對象與另一個不同的文件關聯,
    則必須先關閉(close)現在的文件,然後打開(open)另一個文件:
    要點是在嘗試打開新文件之前,必須先關閉當前的文件流。

open 函數會檢查流是否已經打開。
如果已經打開,則設置內部狀態,以指出發生了錯誤。
接下來使用文件流的任何嘗試都會失敗。

ifstream infile(“in”); // opens file named “in” for reading
infile.close(); // closes “in”
infile.open(“next”); // opens file named “next” for reading

  1. 清除文件流的狀態
    考慮這樣的程序,它有一個 vector 對象,包含一些要打開並讀取的文件名,
    程序要對每個文件中存儲的單詞做一些處理。假設該 vector 對象命名爲files,
    程序也許會有如下循環:

// for each file in the vector
while (it != files.end()) {
ifstream input(it->c_str()); // open the file;

// if the file is ok, read and “process” the input
if (!input)
break; // error: bail out!

while(input >> s) // do the work on this file
process(s);

++it; // increment iterator to get
next file
}

每一次循環都構造了名爲 input 的 ifstream 對象,打開並讀取指定的文件。
構造函數的初始化式使用了箭頭操作符對 it 進行解引用,從而獲取 it 當前表示的 string 對象的 c_str 成員。
文件由構造函數打開,並假設打開成功,讀取文件直到到達文件結束符或者出現其他的錯誤條件爲止。

在這個點上,input 處於錯誤狀態。任何讀 input 的嘗試都會失敗。
因爲 input是 while 循環的局部變量,在每次迭代中創建。
這就意味着它在每次循環中都以乾淨的狀態即 input.good() 爲 true,開始使用。

如果希望避免在每次 while 循環過程中創建新流對象,可將 input 的定義移到 while 之前。
這點小小的改動意味着必須更仔細地管理流的狀態。
如果遇到文件結束符或其他錯誤,將設置流的內部狀態,以便之後不允許再對該流做讀寫操作。
關閉流並不能改變流對象的內部狀態。如果最後的讀寫操作失敗了,對象的狀態將保持爲錯誤模式,
直到執行 clear 操作重新恢復流的狀態爲止。調用 clear 後,就像重新創建了該對象一樣。

如果打算重用已存在的流對象,那麼 while 循環必須在每次循環進記得關閉(close)和清空(clear)文件流:

ifstream input;
vector::const_iterator it = files.begin();

// for each file in the vector
while (it != files.end()) {
input.open(it->c_str()); // open the file

// if the file is ok, read and “process” the input
if (!input)
break; // error: bail out!

while(input >> s) // do the work on this file
process(s);

input.close(); // close file when we’re done with it
input.clear(); // reset state to ok
++it; // increment iterator to get next file
}

如果忽略 clear 的調用,則循環只能讀入第一個文件。
要了解其原因,就需要考慮在循環中發生了什麼:
首先打開指定的文件。假設打開成功,則讀取文件直到文件結束或者出現其他錯誤條件爲止。
在這個點上,input 處於錯誤狀態。
如果在關閉(close)該流前沒有調用 clear 清除流的狀態,接着在 input 上做的任何輸入運算都會失敗。
一旦關閉該文件,再打開 下一個文件時,
在內層while 循環上讀 input 仍然會失敗——畢竟最後一次對流的讀操作到達了文件結束符,
事實上該文件結束符對應的是另一個與本文件無關的其他文件。

如果程序員需要重用文件流讀寫多個文件,必須在讀另一個文件之前調用 clear 清除該流的狀態。

8.4.2. 文件模式
在打開文件時,無論是調用 open 還是以文件名作爲流初始化的一部分,都需指定文件模式(file mode)。
每個 fstream 類都定義了一組表示不同模式的值,用於指定流打開的不同模式。
與條件狀態標誌一樣,文件模式也是整型常量,在打開指定文件時,可用位操作符設置一個或多個模式。
文件流構造函數和 open 函數都提供了默認實參(第 7.4.1 節)設置文件模式。
默認值因流類型的不同而不同。此外,還可以顯式地以模式打開文件。
表 8.3 列出了文件模式及其含義。

表 8.3 文件模式
in 打開文件做讀操作
out 打開文件做寫操作
app 在每次寫之前找到文件尾
ate 打開文件後立即將文件定位在文件尾
trunc 打開文件時清空已存在的文件流
binary 以二進制模式進行 IO 操作

out、trunc 和 app 模式只能用於指定與 ofstream 或 fstream 對象關聯的文件;
in 模式只能用於指定與 ifstream 或 fstream 對象關聯的文件。
所有的文件都可以用 ate 或 binary 模式打開。
ate 模式只在打開時有效:
文件打開後將定位在文件尾。以 binary 模式打開的流則將文件以字節序列的形式處理,而不解釋流中的字符。

默認時,與 ifstream 流對象關聯的文件將以 in 模式打開,該模式允許文件做讀的操作:
與 ofstream 關聯的文件則以 out 模式打開,使文件可寫。
以out 模式打開的文件會被清空:丟棄該文件存儲的所有數據。
從效果來看,爲 ofstream 對象指定 out 模式等效於同時指定了 out 和 trunc 模式。
對於用 ofstream 打開的文件,要保存文件中存在的數據,唯一方法是顯式地指定 app 模式打開:

// output mode by default; truncates file named “file1”
ofstream outfile(“file1”);

// equivalent effect: “file1” is explicitly truncated
ofstream outfile2(“file1”, ofstream::out | ofstream::trunc);

// append mode; adds new data at end of existing file named “file2”
ofstream appfile(“file2”, ofstream::app);

outfile2 的定義使用了按位或操作符將相應的文件同時以out 和 trunc 模式打開。

  1. 對同一個文件作輸入和輸出運算
    fstream 對象既可以讀也可以寫它所關聯的文件。fstream 如何使用它的文件取決於打開文件時指定的模式。

默認情況下,fstream 對象以 in 和 out 模式同時打開。
當文件同時以 in和 out 打開時不清空。
如果打開 fstream 所關聯的文件時,只使用 out 模式,而不指定 in 模式,則文件會清空已存在的數據。
如果打開文件時指定了 trunc模式,則無論是否同時指定了 in 模式,文件同樣會被清空。

下面的定義將copyOut 文件同時以輸入和輸出的模式打開:

// open for input and output
fstream inOut(“copyOut”, fstream::in | fstream::out);

  1. 模式是文件的屬性而不是流的屬性
    每次打開文件時都會設置模式

ofstream outfile;

// output mode set to out, “scratchpad” truncated
outfile.open(“scratchpad”, ofstream::out);
outfile.close(); // close outfile so we can rebind it

// appends to file named “precious”
outfile.open(“precious”, ofstream::app);
outfile.close();

// output mode set by default, “out” truncated
outfile.open(“out”);

第一次調用 open 函數時,指定的模式是 ofstream::out。
當前目錄中名爲“scratchpad”的文件以輸出模式打開並清空。而名爲“precious”的文件,則
要求以添加模式打開:
保存文件裏的原有數據,所有的新內容在文件尾部寫入。
在打開“out”文件時,沒有明確指明輸出模式,該文件則以 out 模式打開,
這意味着當前存儲在“out”文件中的任何數據都將被丟棄。

只要調用 open 函數,就要設置文件模式,其模式的設置可以是顯式的也可以是隱式的。
如果沒有指定文件模式,將使用默認值。

  1. 打開模式的有效組合
    並不是所有的打開模式都可以同時指定。
    有些模式組合是沒有意義的,例如同時以 in 和 trunc 模式打開文件,準備讀取所生成的流,
    但卻因爲 trunc 操作而導致無數據可讀。

表 8.4 列出了有效的模式組合及其含義。
表 8.4 文件模式的組合
out 打開文件做寫操作,刪除文件中已有的數據
out | app 打開文件做寫操作,在文件尾寫入
out | trunc 與 out 模式相同
in 打開文件做讀操作
in | out 打開文件做讀、寫操作,並定位於文件開頭處
in | out | trunc 打開文件做讀、寫操作,刪除文件中已有的數據

上述所有的打開模式組合還可以添加 ate 模式。
對這些模式添加 ate 只會改變文件打開時的初始化定位,在第一次讀或寫之前,將文件定位於文件末尾處。

8.4.3. 一個打開並檢查輸入文件的程序
由於需要在多個程序裏做這件工作,我們編寫一個名爲 open_file 的函數實現這個功能。
這個函數有兩個引用形參,分別是 ifstream 和 string 類型,其中 string 類型的引用形參存儲
與指定 ifstream 對象關聯的文件名:

// opens in binding it to the given file
ifstream& open_file(ifstream &in, const string &file)
{
in.close(); // close in case it was already open
in.clear(); // clear any existing errors

// if the open fails, the stream will be in an invalid state
in.open(file.c_str()); // open the file we were given
return in; // condition state is good if open succeeded
}

由於不清楚流 in 的當前狀態,因此首先調用 close 和 clear 將這個流設置爲有效狀態。
然後嘗試打開給定的文件。如果打開失敗,流的條件狀態將標誌這個流是不可用的。
最後返回流對象 in,此時,in 要麼已經與指定文件綁定起來了,要麼處於錯誤條件狀態。

8.5. 字符串流
iostream 標準庫支持內存中的輸入/輸出,只要將流與存儲在程序內存中的string 對象捆綁起來即可。
此時,可使用 iostream 輸入和輸出操作符讀寫這個 string 對象。

標準庫定義了三種類型的字符串流:
? istringstream,由 istream 派生而來,提供讀 string 的功能。
? ostringstream,由 ostream 派生而來,提供寫 string 的功能。
? stringstream,由 iostream 派生而來,提供讀寫 string 的功能。

要使用上述類,必須包含 sstream 頭文件。
與 fstream 類型一樣,上述類型由 iostream 類型派生而來,
這意味着iostream 上所有的操作適用於 sstream 中的類型。sstream 類型除了繼承的操作外,
還各自定義了一個有 string 形參的構造函數,
這個構造函數將 string類型的實參複製給 stringstream 對象。
對 stringstream 的讀寫操作實際上讀寫的就是該對象中的 string 對象。
這些類還定義了名爲 str 的成員,用來讀取或設置 stringstream 對象所操縱的 string 值。

注意到儘管 fstream 和 sstream 共享相同的基類,但它們沒有其他相互關係。
特別是,stringstream 對象不使用 open 和 close 函數,而 fstream 對象則不允許使用 str。

表 8.5. stringstream 特定的操作
stringstream strm; 創建自由的 stringstream 對象
stringstream
strm(s);

創建存儲 s 的副本的 stringstream 對象,
其中 s 是string 類型的對象strm.str() 返回 strm 中存儲的 string 類型對象
strm.str(s) 將 string 類型的 s 複製給 strm,返回 void

  1. stringstream 對象的和使用
    前面已經見過以每次一個單詞或每次一行的方式處理輸入的程序。
    第一種程序用 string 輸入操作符,
    而第二種則使用 getline 函數。

然而,有些程序需要同時使用這兩種方式:
有些處理基於每行實現,而其他處理則要操縱每行中每個單詞。

可用 stringstreams 對象實現:
string line, word; // will hold a line and word from input, respectively

while (getline(cin, line)) { // read a line from the input into line

// do per-line processing
istringstream stream(line); // bind to stream to the line we read

while (stream >> word){ // read a word from line
// do per-word processing
}
}

這裏,使用 getline 函數從輸入讀取整行內容。然後爲了獲得每行中的單詞,
將一個 istringstream 對象與所讀取的行綁定起來,
這樣只需要使用普通的 string 輸入操作符即可讀出每行中的單詞

  1. stringstream 提供的轉換和/或格式化
    stringstream 對象的一個常見用法是,需要在多種數據類型之間實現自動格式化時使用該類類型。

例如,有一個數值型數據集合,要獲取它們的 string 表示形式,或反之。
sstream 輸入和輸出操作可自動地把算術類型轉化爲相應的string 表示形式,反過來也可以。

int val1 = 512, val2 = 1024;
ostringstream format_message;

// ok: converts values to a string representation
format_message << “val1: ” << val1 << “\n”<< “val2: ” << val2 << “\n”;

這裏創建了一個名爲 format_message 的 ostringstream 類型空對象,並將指定的內容插入該對象。
重點在於 int 型值自動轉換爲等價的可打印的字符串。

format_message 的內容是以下字符:
val1: 512\nval2: 1024

相反,用 istringstream 讀 string 對象,即可重新將數值型數據找回來。
讀取 istringstream 對象自動地將數值型數據的字符表示方式轉換爲相應的算術值。

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章