"流"就是"流動",是物質從一處向另一處流動的過程,比如我們能感知到的水流。C++的流是指信息從外部輸入設備(如鍵盤和磁盤)向計算機內部(即內存)輸入和從內存向外部輸出設備(如顯示器和磁盤)輸出的過程,這種輸入輸出過程被形象地比喻為"流"。
為了實現信息的內外流動,C++系統定義了I/O類庫,其中的每一個類都稱作相應的流或流類,用以完成某一方面的功能。根據一個流類定義的對象也時常被稱為流。
通常標准輸入或標准輸出設備顯示器稱為標准流;外存磁盤上文件的輸入輸出稱為文件流;對於內存中指定的字符串存儲空間稱為字符串流。
那麼流的內容通常是什麼呢?
流裡的基本單位是字節,所以又稱為字節流。字節流可以是ASCII字符、二進制數據、圖形圖像、音頻視頻等信息。文件和字符串也可以看成是有序的字節流,又稱為文件流和字符串流。
C++的IO類庫屬於STL的一部分,在STL中定義了一個龐大的類庫,它們的繼承關系為下圖:
標准輸出,用於正常的輸出,cerr用來輸出警告和錯誤信息,因為被稱為標准錯誤,而clog用來輸出程序運行時的一般性信息。cerr和clog之間的不同之處在於cerr是不經過緩沖區直接向顯示器輸出有關信息,而clog則是先把信息放在緩沖區,緩沖區滿後或遇上endl時向顯示器輸出。
管理文件流的類為:ifstream(文件輸入)、ofstream(文件輸出)和fstream(文件的輸入/輸出)。其中ifstream是從istream中繼承的類,ofstream是從ostream中繼承的類,fstream是從iostream繼承的類。
管理字符串流的類為:istringstream(字符串輸入)、ostringstream(字符串輸出)和stringstream(字符串的輸入/輸出)。其中istringstream是從istream中繼承的類,ostringstream是從ostream中繼承的類,stringstream是從iostream繼承的類。
在istream輸入流類中定義有對右移操作符>>重載的一組公用成員函數,函數的具體聲明格式為:
istream& operator>> (istream& is, char& c); istream& operator>> (istream& is, signed char& c); istream& operator>> (istream& is, unsigned char& c); istream& operator>> (istream& is, char* s); istream& operator>> (istream& is, signed char* s); istream& operator>> (istream& is, unsigned char* s);
由於右移操作符重載用於給變量輸入數據的操作,所以又稱為提取操作符,即從流中提取出數據賦給變量。
當系統執行cin>>variable操作時,將根據實參x的類型調用相應的提取操作符重載函數,把variable引用傳送給對應的形參,接著從鍵盤的輸入中讀入一個值並賦給variable後,返回cin流,以便繼續使用提取操作符為下一個變量輸入數據。
當從鍵盤上輸入數據時,只有當輸入完數據並按下回車鍵後,系統才把該行數據存入到鍵盤緩沖區,供cin流順序讀取給變量。還有,從鍵盤上輸入的每個數據之間必須用空格或回車符分開,因為cin為一個變量讀入數據時是以空格或回車符作為其結束標志的。
當cin>>str_ptr操作中的str_ptr為字符指針類型時,則要求從鍵盤的輸入中讀取一個字符串,並把它賦值給str_ptr所指向的存儲空間中,若str_ptr沒有事先指向一個允許寫入信息的存儲空間,則無法完成輸入操作。另外從鍵盤上輸入的字符串,其兩邊不能帶有雙引號定界符,若帶有只作為雙引號字符看待。對於輸入的字符也是如此,不能帶有單引號定界符。
在ostream輸出流類中定義有對左移操作符<<重載的一組公用成員函數,函數的具體聲明格式為:
istream& operator>> (bool& val); istream& operator>> (short& val); istream& operator>> (unsigned short& val); istream& operator>> (int& val); istream& operator>> (unsigned int& val); istream& operator>> (long& val); istream& operator>> (unsigned long& val); istream& operator>> (long long& val); istream& operator>> (unsigned long long& val); istream& operator>> (float& val); istream& operator>> (double& val); istream& operator>> (long double& val); istream& operator>> (void*& val);
除了與在istream流類中聲明右移操作符重載函數給出的所有內置類型以外,還增加一個void* 類型,用於輸出任何指針(但不能是字符指針,因為它將被作為字符串處理,即輸出所指向存儲空間中保存的一個字符串)的值。
由於左移操作符重載用於向流中輸出表達式的值,所以又稱為插入操作符。如當輸出流是cout時,則就把表達式的值插入到顯示器上,即輸出到顯示器顯示出來。
當系統執行cout<<variable操作時,首先根據x值的類型調用相應的插入操作符重載函數,把variable的值按值傳送給對應的形參,接著執行函數體,把variable的值(亦即形參的值)輸出到顯示器屏幕上,從當前屏幕光標位置起顯示出來,然後返回cout流,以便繼續使用插入操作符輸出下一個表達式的值。
IO操作都有可能發生錯誤,一些錯誤是可恢復的,而其他錯誤發生在系統深處,已經超出了應用程序可以修正的范圍。
IO類定義了一些函數和標志,可以幫助我們訪問和操縱流的條件狀態。
首先表示一個流當前的狀態的變量的類型為strm::iostate,其中strm是一種流類型,可以是iostream、fstream等。比如,我們定義一個標准IO流狀態:
iostream::iostate strm_state=iostream::goodbit;
IO庫存定義了4個iostate類型的contexpr值,表示特定的位模式。這些值用來表示特定類型的IO條件,可以與位運算一起使用來一次性檢測或設置多個標志位。
1)strm::badbit用來指定流已崩潰。它表示系統級的錯誤,如不可恢復的讀寫錯誤。通常情況下,一旦badbit被置位,流就無法再使用了。
2)strm::failbit用來指出一個IO操作失敗了。
3)strm::eofbit用來指出流達了文件的結束。
在發生可恢復錯誤後,failbit被置位,如期望讀取數值卻讀出一個字符錯誤。這種問題通常可以修正,流還可以繼續使用。如果到達文件結束位置,eofbit和failbit都會被置位。
4)strm::goodbit用來指出流未處於錯誤狀態。此值保證為零。
goodbit的值為0,表示流未發生錯誤。如果badbit、failbit和eofbit任一個置位,則檢測流狀態的條件會失敗。
標准庫還定義了一組函數來查詢這些標志位的狀態,假如s是一個流,那麼:
s.eof() // 若流s的eofbit置位,則返回true s.fail() // 若流s的failbit或badbit置位,則返回true s.bad() // 若流s的badbit被置位,則返回true s.good() // 若流s處於有效狀態,則返回true
在實際我們在循環中判斷流的狀態是否有效時,都直接使用流對象本身,比如:while(cin>>variable){cout<<variable},在實際中都轉換為了while((cin>>variable).good()){cout<<variable}。
IO類庫提供了3個函數來管理和設置流的狀態:
s.clear(); // 將流s中所有條件狀態復位,將流的狀態設置為有效,調用good會返回true s.clear(flags); // 根據給定的flags標志位,將流s中對應的條件狀態復位 s.setstate(flags); // 根據給定的flags標志位,將流s中對應的條件狀態置位。 s.rdstate(); // 返回一個iostate值,對應流當前的狀態。
我們可以這樣使用上面的這些成員函數。
iostream::iostate old_state = cin.rdstate(); // 記住cin當前的狀態 cin.clear(); // 使用cin有效 process_input(cin); // 使用cin cin.setstate(old_state); // 將cin置為原有狀態 cin.clear(cin.rdstate() & ~cin.failbit & ~cin.badbit); // 下failbit和badbit復位,保持eofbit不變。
我們先看一個簡單的輸入輸出程序:
int main() { char ch; while (cin >> ch && ch!='#') { cout << ch; } return 0; }
程序的功能是,循環輸入字符,然後把輸入的字符顯式出來,遇到#或cin流失敗時結束,按照程序的表面來看,我們想要的效果是輸入一個,顯示一個,像這樣rroonnyy#,紅色代表的是顯示的結果。而實際中我們的輸出與輸出卻是這樣的:
ronny#abc [Enter]
ronny
輸入字符立即回顯是非緩沖或直接輸入的一個形式,它表示你所鍵入的字符對正在等待的程序立即變為可用。相反,延遲回顯是緩沖輸入的例子,這種情況下你所鍵入的字符塊被收集並存儲在一個被稱為緩沖區的臨時存儲區域中。按下回車鍵可使你輸入的字符段對程序起作用。
緩沖輸入一般常用在文本程序內,當你輸入有錯誤時,就可以使用你的鍵盤更正修正錯誤。當最終按下回車鍵時,你就可以發送正確的輸入。
而在一些交互性的游戲裡需要非緩沖輸入,如:游戲裡你按下一個鍵時就要執行某個命令。
緩沖分為兩類:
1)完全緩沖:緩沖區被充滿時被清空(內容發送到其目的地)。這種類型的緩沖通常出現在文件輸入中。
2)行緩沖:遇到一個換行字符時被清空緩沖區。鍵盤的輸入是標准的行緩沖,因此按下回車鍵將清空緩沖區。
上面講的是輸入的緩沖,而C++中的輸出也是存在緩沖的。
每個輸出流都管理一個緩沖區,用來保存程序讀寫的數據。例如,如果執行下面的代碼
os<<”please enter a value:”;
文本串可能立即打印出來,但也有可能被操作系統保存在緩沖區中,隨後再打印。有了緩沖機制,操作系統就可以將程序的多個輸出操作組合成單一的系統級寫操作。由於設備的寫操作可能很耗時,允許操作系統將多個輸出操作組合為單一的設備寫操作可以帶來很大的性能提升。
導致緩沖刷新(即,數據真正寫到輸出設備或文件)的原因有很多:
<1> 程序正常結束,作為main函數的return操作的一部分,緩沖刷新被執行。
<2> 緩沖區滿時,需要刷新緩沖,而後新的數據才能繼續寫入緩沖區。
<3> 我們可以使用操縱符endl來顯式刷新緩沖區。
<4> 在每個輸出之後,我們可以用操縱符unitbuf設置流的內部狀態,來清空緩沖區。默認情況下,對cerr是設置unitbuf的,因此寫到cerr的內容都是立即刷新的。
<5> 一個輸出流被關聯到另一個流。在這種情況下,當讀寫被關聯的流時,關聯到的流的緩沖區會被刷新,cin和cerr都關聯到cout。因此讀cin或寫cerr會導致cout的緩沖區被刷新。
除了endl可以完成換行並刷新緩沖區外,IO庫中還有兩個類似的操縱符:flush和ends。flush刷新緩沖區,但不輸出,任何額外的字符;ends向緩沖區插入一個空字符,然後刷新緩沖區。
cout << "hi!" << endl; // 輸出 hi 和一個換行符,然後刷新緩沖區 cout << "hi!" << flush; // 輸出hi,然後刷新緩沖區,不附加任何額外字符 cout << "hi!" << ends; // 輸出hi和一個空字符。然後刷新緩沖區
如果想每次輸出操作後都刷新緩沖區,我們可以使用unitbuf操縱符。它告訴流在接下來的每次寫操作後都進行一次flush操作。而nounitbuf操作符則重置流使其恢復使用正常的系統管理的緩沖區刷新機制。
cout << unitbuf; // 所有輸出操作後都立即刷新緩沖區 // 任何輸出都立即刷新,無緩沖 cout << nounitbuf; // 回到正常的緩沖方式
注意:如要程序異常終止,輸出緩沖區是不會被刷新的。當一個程序崩潰後,它所輸出的數據很可能停留在輸出緩沖區中等待打印。
我們可以將一個istream流關聯到另一個ostream,也可以將一個ostream流關聯到另一個ostream。
cin.tie(&cout); // 標准庫已經將cin與cout關聯在一起 // s.tie如果s關聯到一個輸出流,則返回指向這個流的指針,如果對象未關聯到流,則返回空指針 ostream *old_tie = cin.tie(nullptr); // 將cin不再與其他流關聯,同時old_tie指向cout cin.tie(&cerr); // 讀取cin會刷新cerr而不是cout cin.tie(old_tie); // 重建cin和cout的正常關聯
6.1 使用文件流對象
創建一個文件流對象時,我們可以提供文件名,也可不提供文件名,後面用open成員函數來打開文件。
string infile="../input.txt"; string outfile = "../output.txt"; ifstream in(infile); // 定義時打開文件 ofstream out; out.open(outfile); // 用open打開文件
如果調用open失敗,failbit會被置位,所以調用open時進行檢測通常是一個好習慣。
如果用一個讀文件流ifstream去打開一個不存在的文件,將導致讀取失敗,而如果用一寫文件流ofstream去打開一個文件,如果文件不存在,則會創建這個文件。
一旦一個文件流已經被打開,它就保持與對應文件的關聯。實際上,對一個已經打開的文件流調用open會失敗,並會導致failbit被置位,隨後的試圖使用文件流的操作都會失敗。為了將文件流關聯到另外一個文件,必須首先關閉已經關聯的文件。關閉一個流的關聯文件可以用close成員函數來完成。
每個流都有一個關聯的文件模式,用來指出如何使用文件,下面列出了文件模式和它們的含義:
in
以讀方式打開
out
以寫方式打開
app
每次寫操作均定位到文件末尾
ate
打開文件後立即定位到文件末尾
trunc
截斷文件
binary
以二進制方式進行IO
用文件名初始化一個流時或用open打開文件時都可以指定文件模式,但要注意下面幾種限制:
<1>只可以對ofstream或fstream對象設定out模式。
<2>只可以對ifstream或fstream對象設定in模式。
<3>只有out設定時才可以設定trunc模式。
<4>只要trunc沒有被設定,就可以設定app模式。在app模式下,即使沒有顯式指定out模式 ,文件也總是以輸出方式被打開。
<5>默認情況下,即使我們沒有指定trunc,以out模式打開的文件也會被截斷。為了保留以out模式打開的文件的內容,我們必須同時指定app模式,這樣只會將數據追加寫到文件末尾;或者同時指定in模式,即打開文件同時進行讀寫操作。
<6>ate和binary模式可以用於任何類型的文件流對象,且可以與其他任何文件模式組合使用。
以out模式打開文件會丟棄已有數據,所以阻止一個ofstream清空給定文件內容的方法是同時指定app模式。
// 在這幾條語句中,file1都被截斷 ofstream out("file1"); // 隱含以輸出模式打開文件並截斷文件 ofstream out2("file1", ofstream::out); // 隱含地截斷文件 ofstream out3("file1", ofstream::out | ofstream::trunc); // 顯式截斷文件 // 為了保留文件內容,我們必須顯式指定app ofstream app("file2", ofstream::app); // 隱含為輸出模式 ofstream app("file2", ofstream::app | ofstream::out);
sstream頭文件定義了三個類型來支持內存IO,這些類型可以向string寫入數據,從string讀取數據,就像string是一個IO流一樣。
很多時候我們需要逐行處理文本,而且需要對行內的單詞進行單獨分析,這時候使用istringstream是很方便的。
比如,我們程序需要一次讀取一行文本,然後將其中的單詞分別取出保存在一個vector中。
string line,word; while (getline(cin, line)) { vector<string> wordList; istringstream lineText(line); while (lineText >> word) { wordList.push_back(word); } }
當我們逐步構造輸出時,希望最後一起打印時,ostringstream是很有用的,它可以幫我們完成類似於itoa,ftoa這種數字轉字符串的功能。
int num1 = 42; double pi = 3.1415926; string str = "some numbers"; ostringstream formatted; formatted << str << pi << num1; cout << formatted.str() << endl;
其中str成員函數是stringstream有幾個特有操作之一。
string s; stringstream strm(s);// 保存s的一個拷貝,此構造函數是explicit的。 strm.str(); // 返回strm所保存的string對象的拷貝。 strm.str(s); // 將s拷貝到strm中,返回void。
標准庫定義了一組操縱符用來修改流的狀態,一個操縱符是一個函數或是一個對象,會影響流的狀態,並能用作輸入或輸出運算符的運算對象,比如我們熟悉的endl,就是一個操縱符。
操縱符用於兩大類輸出控制:控制數值的輸出形式以及控制補白的數量和位置,大多數改變格式狀態的操縱符都是設置/復原成對的:一個操縱符用來將格式狀態設置為一個新值,而另一個用來將其復原,恢復為正常默認格式。
通過設置boolalpha可以將bool型變量的true輸出為true或將false輸出為false。可以設置noboolalpha來將內部狀態恢復為默認格式。
// modify boolalpha flag #include <iostream> // std::cout, std::boolalpha, std::noboolalpha int main () { bool b = true; std::cout << std::boolalpha << b << '\n'; std::cout << std::noboolalpha << b << '\n'; return 0; }
默認情況是以十進制格式輸出,我們可以設置不同的格式操縱符來改變輸出整型值的進制。
oct:以八進制顯示
hex:以十六進制顯示
dec:以十進制顯示
另外可以使用showbase操縱符來顯式格式的前綴,8進制前有前導0,十六進制有前導0x。操縱符noshowbase恢復cout的狀態,從而不再顯示整型值的進制。有時候我們需要將16進制輸出為大寫如0X FF,可以用操縱符uppercase和nouppercase來控制流輸出的大小寫狀態。
cout << uppercase << showbase << hex << 20 << 1024 << nouppercase << noshowbase << dec << endl;
打印精度是通過precision成員或使用setprecision操縱符來改變。其中precision是一個重載函數,一個版本接受int參數,將精度設置為此值,並返回舊精度值。另外一個版本不接受參數,返回當前精度值。setprecision操縱符接受一個參數,用來設置精度。
用scientific用來指定科學記數法,fixed指定為定點十進制,hexfloat指定為十六進制的浮點數。defaultfloat將流恢復到默認的狀態。
設置showpoint可以用來強制打印小數。
setw:指定下一個數字或字符串的最小空間
left:表示左對齊輸出。
right:表示右對齊輸出,右對齊是默認格式。
internal:控制負數的符號的位置,它左對齊符號,右對齊值,用空格填滿所有中間空間。
setfill:允許指定一個字符代替默認的空格來補白輸出。
1 int i = -16; 2 double d = 3.14159; 3 cout << "i:" << setw(12) << i << '\n' 4 << "d:" << setw(12) << d << '\n'; 5 cout << left 6 << "i:" << setw(12) << i << '\n' 7 << "d:" << setw(12) << d << '\n'; 8 cout << right 9 << "i:" << setw(12) << i << '\n' 10 << "d:" << setw(12) << d << '\n'; 11 cout << internal 12 << "i:" << setw(12) << i << '\n' 13 << "d:" << setw(12) << d << '\n'; 14 cout << setfill('#') 15 << "i:" << setw(12) << i << '\n' 16 << "d:" << setw(12) << d << '\n' 17 << setfill(' ');
seekg(new_position); seekp (new_position); seekg( offset, dir); seekp( offset, dir);
第一個版本將當前位置切換到給定地點,第二個版本接受一個偏移量以及從何處計算偏移的指示器。
tell函數返回的一個值,使用適當類的pos_type成員來保存。
假定給定一個文件來讀,我們將在文件的末尾寫一個新行,改行包含了每一行開頭的相對位置(程序不必寫第一行的偏移量,因為它總是0)。例如給定下面的文件,
abcd
efg
hi
j
這段程序應產生修改過的文件如下:
abcd
efg
hi
j
5 9 12 14
1 #include <iostream> 2 #include <fstream> 3 #include <string> 4 5 using std::fstream; 6 using std::cerr; 7 using std::endl; 8 using std::ifstream; 9 using std::ofstream; 10 using std::string; 11 using std::getline; 12 using std::cout; 13 //using namespace std; 14 15 int main() 16 { 17 fstream inOut("copyOut.txt", 18 fstream::ate | fstream::in | fstream::out); //用ate方式打開,會將文件的位置定位到文件末尾。 19 20 if( !inOut ) { 21 cerr << "unable to open file" <<endl; 22 return EXIT_FAILURE; 23 } 24 25 inOut.seekg(-1,fstream::end); //go to the last char 26 if( inOut.peek() != 10) //if the last char of the file is not a newline,add it. 27 { 28 inOut.seekg(0,fstream::end); 29 inOut.put('\n'); 30 } 31 32 inOut.seekg(0,fstream::end); 33 ifstream::pos_type endMark = inOut.tellg(); //record the last position . 34 35 inOut.seekg(0,fstream::beg); 36 37 int cnt = 0; //accumulator for byte count 38 string line; //hold each line of input 39 40 while( inOut && inOut.tellg() != endMark 41 && getline(inOut , line) 42 ) 43 { 44 cnt += line.size() + 1; // add 1 to acount for the newline 45 46 ifstream::pos_type mark = inOut.tellg(); 47 inOut.seekp( 0, fstream::end); //set write marker to end 48 inOut << cnt; 49 50 if( mark != endMark) inOut << " "; 51 inOut.seekg(mark); //restore read position 52 } 53 inOut.clear(); //clear flags in case we hit an error 54 inOut.seekp(0 , fstream::end); //seek to end 55 inOut << endl; //write a newline at end of file 56 57 return 0; 58 }