C++的那些事:流與IO類

1、流的概念

"流"就是"流動",是物質從一處向另一處流動的過程,比如我們能感知到的水流。C++的流是指信息從外部輸入設備(如鍵盤和磁盤)向計算機內部(即內存)輸入和從內存向外部輸出設備(如顯示器和磁盤)輸出的過程,這種輸入輸出過程被形象地比喻爲"流"。

爲了實現信息的內外流動,C++系統定義了I/O類庫,其中的每一個類都稱作相應的流或流類,用以完成某一方面的功能。根據一個流類定義的對象也時常被稱爲流。

通常標準輸入或標準輸出設備顯示器稱爲標準流;外存磁盤上文件的輸入輸出稱爲文件流;對於內存中指定的字符串存儲空間稱爲字符串流。

那麼流的內容通常是什麼呢?

流裏的基本單位是字節,所以又稱爲字節流。字節流可以是ASCII字符、二進制數據、圖形圖像、音頻視頻等信息。文件和字符串也可以看成是有序的字節流,又稱爲文件流和字符串流。

2、IO類

C++的IO類庫屬於STL的一部分,在STL中定義了一個龐大的類庫,它們的繼承關係爲下圖:

image

管理標準的輸入/輸出流的類爲:istream(輸入)、ostream(輸出)、iostream(輸入輸出),其中istream和ostream直接從ios中繼承,iostream多重繼承了istream和otream。而cin是STL中定位的一個用於輸入的istream對象,cout、cerr、clog是三個用於輸出的ostream對象。其中cout對象也被稱爲標準輸出,用於正常的輸出,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繼承的類。

3,<<和>>操作符

3.1 <<的用法

在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沒有事先指向一個允許寫入信息的存儲空間,則無法完成輸入操作。另外從鍵盤上輸入的字符串,其兩邊不能帶有雙引號定界符,若帶有隻作爲雙引號字符看待。對於輸入的字符也是如此,不能帶有單引號定界符。

3.2 >>的用法

在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流,以便繼續使用插入操作符輸出下一個表達式的值。

4,IO條件狀態

4.1 查詢流的狀態

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}。

4.2 管理條件狀態

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不變。

5,IO緩衝區

5.1 輸入緩衝

我們先看一個簡單的輸入輸出程序:

int main() 
{ 
    char ch; 
    while (cin >> ch && ch!='#') 
    { 
        cout << ch; 
    } 
    return 0; 
}

程序的功能是,循環輸入字符,然後把輸入的字符顯式出來,遇到#或cin流失敗時結束,按照程序的表面來看,我們想要的效果是輸入一個,顯示一個,像這樣rroonnyy#,紅色代表的是顯示的結果。而實際中我們的輸出與輸出卻是這樣的:

ronny#abc [Enter]

ronny

輸入字符立即回顯是非緩衝或直接輸入的一個形式,它表示你所鍵入的字符對正在等待的程序立即變爲可用。相反,延遲迴顯是緩衝輸入的例子,這種情況下你所鍵入的字符塊被收集並存儲在一個被稱爲緩衝區的臨時存儲區域中。按下回車鍵可使你輸入的字符段對程序起作用。

緩衝輸入一般常用在文本程序內,當你輸入有錯誤時,就可以使用你的鍵盤更正修正錯誤。當最終按下回車鍵時,你就可以發送正確的輸入。

而在一些交互性的遊戲裏需要非緩衝輸入,如:遊戲裏你按下一個鍵時就要執行某個命令。

緩衝分爲兩類:

1)完全緩衝:緩衝區被充滿時被清空(內容發送到其目的地)。這種類型的緩衝通常出現在文件輸入中。

2)行緩衝:遇到一個換行字符時被清空緩衝區。鍵盤的輸入是標準的行緩衝,因此按下回車鍵將清空緩衝區。

5.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,使用文件流

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成員函數來完成。

6.2 文件模式

每個流都有一個關聯的文件模式,用來指出如何使用文件,下面列出了文件模式和它們的含義:

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);

7,使用字符流

sstream頭文件定義了三個類型來支持內存IO,這些類型可以向string寫入數據,從string讀取數據,就像string是一個IO流一樣。

7.1 使用istringstream

很多時候我們需要逐行處理文本,而且需要對行內的單詞進行單獨分析,這時候使用istringstream是很方便的。

比如,我們程序需要一次讀取一行文本,然後將其中的單詞分別取出保存在一個vector中。

string line,word; 
while (getline(cin, line)) 
{ 
    vector<string> wordList; 
    istringstream lineText(line); 
    while (lineText >> word) 
    { 
        wordList.push_back(word); 
    } 
}

7.2 使用ostringstream

當我們逐步構造輸出時,希望最後一起打印時,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。

8,格式化輸入輸出

8.1 操縱符

標準庫定義了一組操縱符用來修改流的狀態,一個操縱符是一個函數或是一個對象,會影響流的狀態,並能用作輸入或輸出運算符的運算對象,比如我們熟悉的endl,就是一個操縱符。

操縱符用於兩大類輸出控制:控制數值的輸出形式以及控制補白的數量和位置,大多數改變格式狀態的操縱符都是設置/復原成對的:一個操縱符用來將格式狀態設置爲一個新值,而另一個用來將其復原,恢復爲正常默認格式。

8.2 控制布爾值的格式

通過設置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;
}

8.3 指定整形值的進制

默認情況是以十進制格式輸出,我們可以設置不同的格式操縱符來改變輸出整型值的進制。

oct:以八進制顯示

hex:以十六進制顯示

dec:以十進制顯示

另外可以使用showbase操縱符來顯式格式的前綴,8進制前有前導0,十六進制有前導0x。操縱符noshowbase恢復cout的狀態,從而不再顯示整型值的進制。有時候我們需要將16進制輸出爲大寫如0X FF,可以用操縱符uppercase和nouppercase來控制流輸出的大小寫狀態。

cout << uppercase << showbase << hex << 20 << 1024 
        << nouppercase << noshowbase << dec << endl;

8.4 控制浮點數格式

打印精度是通過precision成員或使用setprecision操縱符來改變。其中precision是一個重載函數,一個版本接受int參數,將精度設置爲此值,並返回舊精度值。另外一個版本不接受參數,返回當前精度值。setprecision操縱符接受一個參數,用來設置精度。

用scientific用來指定科學記數法,fixed指定爲定點十進制,hexfloat指定爲十六進制的浮點數。defaultfloat將流恢復到默認的狀態。

設置showpoint可以用來強制打印小數。

8.5 輸出補白

setw:指定下一個數字或字符串的最小空間

left:表示左對齊輸出。

right:表示右對齊輸出,右對齊是默認格式。

internal:控制負數的符號的位置,它左對齊符號,右對齊值,用空格填滿所有中間空間。

setfill:允許指定一個字符代替默認的空格來補白輸出。

int i = -16; 
double d = 3.14159; 
cout << "i:" << setw(12) << i << '\n' 
<< "d:" << setw(12) << d << '\n'; 
cout << left 
<< "i:" << setw(12) << i << '\n' 
<< "d:" << setw(12) << d << '\n'; 
cout << right 
<< "i:" << setw(12) << i << '\n' 
<< "d:" << setw(12) << d << '\n'; 
cout << internal 
<< "i:" << setw(12) << i << '\n' 
<< "d:" << setw(12) << d << '\n'; 
cout << setfill('#') 
<< "i:" << setw(12) << i << '\n' 
<< "d:" << setw(12) << d << '\n' 
<< setfill(' ');

image

9,流的隨機訪問

不同的流類型一般支持對相關流中數據的隨機訪問。可以重新定位流,以便環繞跳過,首先讀最後一行,再讀第一行,以此類推。標準庫提供一對函數來定位(seek)給定位置並告訴(tell)相關流中的當前位置。

9.1 seek和tell函數

seekg:重新定位輸入流中的標記

tellg:返回輸入流中標記的當前位置

seekp:重新定位輸出流中的標記

tellp:返回輸出流中標記的當前位置

邏輯上,只能在istream或者ifstream或者istringstream上使用g版本,並且只能在ostream類型或其派生類性ofstream或者ostringstream之上使用p版本。iostream對象,fstream或者stringstream對象對相關流既可以讀也可以寫,可以使用兩個版本的任意版本。9.

9.2 只有一個標記

雖然標準庫區分輸入和輸入而有兩個版本,但它只在文件中維持一個標記——沒有可區分的讀標記和寫標記。

只是試圖在ifstream對象上調用tellp的時候,編譯器將會給出錯誤提示。反之亦然。

使用既可以讀又能寫的fstream類型以及stringstream類型的時候,只有一個保存數據的緩衝區和一個表示緩衝器中當前位置的標記,標準庫將g版本和p版本都映射到這個標記。

9.3 普通iostream對象一般不允許隨機訪問。

9.4 重新定位標記

seekg(new_position);
seekp (new_position);
seekg( offset, dir);
seekp( offset, dir);

第一個版本將當前位置切換到給定地點,第二個版本接受一個偏移量以及從何處計算偏移的指示器。

9.5 訪問標記

tell函數返回的一個值,使用適當類的pos_type成員來保存。

10,一個實例

假定給定一個文件來讀,我們將在文件的末尾寫一個新行,改行包含了每一行開頭的相對位置(程序不必寫第一行的偏移量,因爲它總是0)。例如給定下面的文件,

abcd

efg

hi

j

這段程序應產生修改過的文件如下:

abcd

efg

hi

j

5 9 12 14

#include <iostream>
#include <fstream>
#include <string>

using std::fstream;
using std::cerr;
using std::endl;
using std::ifstream;
using std::ofstream;
using std::string;
using std::getline;
using std::cout;
//using namespace std;

int main()
{
    fstream inOut("copyOut.txt",
        fstream::ate | fstream::in | fstream::out);            //用ate方式打開,會將文件的位置定位到文件末尾。

    if( !inOut )        {
        cerr << "unable to open file" <<endl;
        return EXIT_FAILURE;
    }

    inOut.seekg(-1,fstream::end);            //go to the last char
    if( inOut.peek() != 10)                    //if the last char of the file is not a newline,add it.
    {
        inOut.seekg(0,fstream::end);
        inOut.put('\n');
    }

    inOut.seekg(0,fstream::end);
    ifstream::pos_type endMark = inOut.tellg();        //record the last position .

    inOut.seekg(0,fstream::beg);

    int cnt = 0;                        //accumulator for byte count
    string line;                        //hold each line of input

    while( inOut && inOut.tellg() != endMark
        && getline(inOut , line)
        )
    {
        cnt += line.size() + 1;            // add 1 to acount for the newline

        ifstream::pos_type mark = inOut.tellg();
        inOut.seekp( 0, fstream::end);    //set write marker to end
        inOut << cnt;

        if( mark != endMark) inOut << " ";
        inOut.seekg(mark);                //restore read position
    }
    inOut.clear();                        //clear flags in case we hit an error
    inOut.seekp(0 , fstream::end);        //seek to end
    inOut << endl;                        //write a newline at end of file

    return 0;
}

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