c++進階---IO類的詳細介紹(一)
IO類
c++為了更好的處理不同的種類的IO的操作,IO庫中定義了龐大的類庫來處理不同種類的IO操作,該類庫組成如下圖所示:
首先,我們先了解一下這個龐大的IO庫各個類之間的關係。
- ios是最基本的父類,其中istream類和ostream類都繼承了ios類。
- iostream類通過多重繼承繼承了istream類和ostream類。
- ifstream、istringstream兩個類都是繼承了istream類,oftream、ostringstream兩個類都繼承了ostream類。
OK,另外,我們要知道:
- 處理標準輸入輸出的類:istream(從流中讀取資料,即處理輸入)、ostream(向流中寫入資料,即處理輸出)、iostream(處理輸入和輸出),這些類都被定義在標頭檔案:iostream中。
- 處理檔案的輸入和輸出的類:ifstream(從檔案讀取資料)、ofstream(向檔案寫入資料)、fstream(讀寫檔案),這些類都被定義在標頭檔案:fstream中
- 處理string流的類:istringstream(從string中讀取資料)、ostringstream(向string寫入資料)、stringstream(讀寫string)。然後,這些類都是定義在標頭檔案:sstream中。
另外,我們再補充一下標準輸入輸出中的一些知識:
- cin是STL中定義的一個istream物件,它的作用是用於輸入。
- cout、cerr、clog是STL中定義的一個ostream物件,它們的作用是用於輸出,其中cout是標準輸出,cerr是用於輸出錯誤或者警告資訊,clog是用於輸出程式的一般性資訊。其中cerr不經過緩衝區,直接把錯誤資訊輸出到顯示器中,clog則先把資訊放在緩衝區中。如果我們輸入一個endl,那麼它將會把我們輸出緩衝區的內容全部輸出,並且輸出一個換行。
cin的詳解
程式的每次輸入都會建立一個輸入緩衝區。而我們的cin就是直接從這個輸入緩衝區獲取資料的,如果我們的緩衝區中有資料殘留(不為空時),那麼cin物件直接從緩衝區讀取資料而不是請求輸入。另外,之前我們已經說過了cin是istream定義的一個物件,所以我們每次使用cin進行請求輸入的時候,都是在呼叫istream這個物件的方法,其中istream這個物件處理輸入的方法三種:cin>> 、cin.get() 、cin.getline().另外,cin是可以連續從緩衝區中讀取想要的資料的,它是以(tab 、space、enter)中的一種作為分隔符,
- cin>>方法的介紹
注意事項:
(1)cin>>,其實是呼叫類的:istream & operator >>方法,它很多種過載的版本,分別用於處理字元、浮點數、整型等資料型別的輸入。
(2)cin>>,這個方法遇到了分割符(tab 、space、enter)時,就返回結束該次cin>>方法的呼叫。然後,如果我們呼叫cin>>方法時,從緩衝區讀取資料,如果開頭的資料是分隔符,那麼直接把分割符忽略並且清除,直至第一個字元不是分隔符時才開始輸入。
示例展示:
#include<iostream>
#include<string>
using namespace std;
int main()
{
int num;
string s;
cin >> num >> s;
cout << num << " " << s << endl;
getline(cin,s);
cout << "string:" << s << endl;
cout << endl;
cout << "test" << endl;
cout << endl;
return 0;
}
輸入:
[回車][回車][回車]12 abcd[回車]
輸出:
ps:首先了getline函式是不會忽略開頭的分割符的,我們開始儲存到緩衝區的三個回車都被cin>>方法忽略了,而最後一個回車也在緩衝區中,但是沒有被忽略,而是被geiline函式讀了進來,所以在輸出test之前會有兩個換行。
- cin.get()方法介紹
注意事項:
(1)cin.get()它有四種比較常見的過載格式:
int cin.get(); //不帶引數時,返回值為整型,但遇到了檔案的結束,返回EOF,其實就是:-1
istream & cin.get(char & s) //返回值為輸入流。
istream & cin.get(char * s,streamsize n) //用於輸入一個字串,但是它只接受引數時c語言風格字串風格的引數,即是不接受string類的引數,而且它預設是以換行作為結束符。
istream & cin.get(char * s,streamsize n,char end) //用於輸入一個字串,同樣引數必須是c風格的,當時它是以:字元end,作為結束符。
(2)上面的streamsize在標頭檔案iostream中的定義為long long型的資料型別的別名。所有版本的cin.get()方法是不會忽略開頭的分隔符符號的,而且它和cin>>一樣,遇到分隔符(tab、space 、enter)時,就結束該次方法的呼叫。最後的那個分隔符還是儲存在緩衝區中。
示例展示:
#include<iostream>
using namespace std;
int main()
{
char s;
char s1;
s1 = cin.get();
cin.get(s);
cout << (int)s1 << " " << (int)s << endl;
char str[5] = {NULL};
char end;
cin.get(str,5);
cin.get(end);
cout << str << " " << (int)end << endl;
cout << endl;
return 0;
}
輸入:
[回車][回車]test[回車]
輸出:
ok,我們從結果可以看出,我們開始輸入的兩個回車都被作為s1和s的初始值了,而我們字串的最後一個回車也被end作為初始值了,從而可以看出cin.get()是不會忽略緩衝區中的分隔符的。
- cin.getline()方法的介紹
注意事項:
(1)cin.getline()方法重要的幾個過載版本
//這裡我cin.get後面的兩個方法一樣,用於輸入字串。
//cin.getline不會將最後一個輸入的結束符或者換行符殘留在輸入緩衝區中,
//而是一起輸出到顯示器中。預設遇到‘\n’結束輸入。
istream & cin.getline(char *s ,streamsize n)
//這個方法就是結束方式不同,它是遇到了字元:end就結束輸入。然後,輸出長度為n-1範圍內,end之前的所有字元。
istream & cin.getline(char * s,streamsize n,char end)
程式碼示例:
#include<iostream>
#include<string>
using namespace std;
int main()
{
char s[5];
char t;
cin.getline(s, 5);
cout << s << endl;;
cin >> t;
cout << t << endl;
cout << endl;
return 0;
}
輸入:
new[回車]f
輸出:
ok,從輸出的結果,我們可以發現,cin.getline是把我們輸入緩衝區的那個回車一併輸出來了。
- 補充:getline函式的介紹(用於輸入一行時使用,接受空格的輸入)
(1):C++中定義了一個在std名字空間的全域性函式getline,因為這個getline函式的引數使用了string字串,所以宣告在了string標頭檔案中了。
getline利用cin可以從標準輸入裝置鍵盤讀取一行,當遇到如下三種情況會結束讀操作:1)到檔案結束,2)遇到函式的定界符,3)輸入達到最大限度。
函式原型有兩個過載形式:
istream& getline ( istream& is, string& str);//預設以換行符結束
istream& getline ( istream& is, string& str, char end);//以end字元結束
(2)注意,getline遇到結束符時,會將結束符一併讀入指定的string中,再將結束符替換為空字元。因此,進行從鍵盤讀取一行字元時,建議使用getline,較為安全。但是,最好還是要進行標準輸入的安全檢查,提高程式容錯能力。另外,cin.getline()類似,但是cin.getline()屬於istream流,而getline()屬於string流,是不一樣的兩個函式。
程式碼示例:
#include<iostream>
#include<string>
using namespace std;
int main()
{
string str;
getline(cin, str);
cout << str;
cout << endl;
return 0;
}
輸入:
i am a student;
輸出:
- gets_s 函式的介紹(一般不要用它好了,c中定義的語法),原來是gets,但是在新的vs2015中已經沒有gets了。只有gets_s
gets是C中的庫函式,在< stdio.h>申明,從標準輸入裝置讀字串,可以無限讀取,不會判斷上限,以回車結束或者EOF時停止讀取,所以程式設計師應該確保buffer的空間足夠大,以便在執行讀操作時不發生溢位。
函式原型:char *gets_s( char *buffer );
程式碼示例:
#include<iostream>
using namespace std;
int main()
{
char array[20] = { NULL };
gets_s(array);
cout << array << endl;
return 0;
}
輸入:
new find
輸出:
重點內容補充:IO類的物件都是不可以拷貝的,所以如果我們需要使用IO類的物件作為函式的引數的時候或者返回值的時候,我們都要使用引用型別
條件狀態
因為我們在使用IO類的時候,總會出現一些錯誤,於是每個IO類都定義了一個iostate這個資料型別表示當前某個操作所處的狀態。其中IO庫定義了4個iostate型別的constexpr的值,它們分別是:
strm::badbit :首先,我們要說明strm代表的是IO類中的一種,例如istream、ostream、fstream等,babit表示系統級的錯誤,如果IO發生了不可修復的錯誤,badbit將被置位,而且流將無法再使用。
strm::failbit:這個會在流發生可修復的錯誤時,failbit將會被置位,例如:當我們本來是要求輸入一個數值的卻輸入了一個字元等錯誤,這種問題可以被修正的,流還可以繼續被使用。
strm::eofbit:當流達到了檔案的結束的位置,eofbit和failbit這兩個狀態都會被置位。
strm::goodbit:當goodbit的值為1時,表示流未發生錯誤。
最後就是,只要badbit、eofbit、failbit任意一個狀態被置位,那麼一切以流狀態為條件的語句都是返回false。例如:while(cin>>word),這條語句,只要上述三個中,有一個被置位,迴圈就結束。
IO庫中還定義了一組函式,用於查詢當前某種狀態的值。具體函式如下:其中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<
#include<iostream>
using namespace std;
int main()
{
int t;
while (cin>>t)
{
cout << "cin.good的值:"<<cin.good() << endl;
cout << "cin.eof的值:" << cin.eof() << endl;
cout << "cin.fail的值:" << cin.fail() << endl;
cout << "cin.bad的值:" << cin.bad() << endl;
}
cout << "結束後" << endl;
cout << "cin.good的值:" << cin.good() << endl;
cout << "cin.eof的值:" << cin.eof() << endl;
cout << "cin.fail的值:" << cin.fail() << endl;
cout << "cin.bad的值:" << cin.bad() << endl;
system("pause");
return 0;
}
輸入:1[空格]2[空格]d[回車]
輸出:
IO類庫還提供了3個函式來管理和設定流的狀態:
s.clear(); // 將流s中所有條件狀態復位,將流的狀態設定為有效,呼叫good會返回true
s.clear(flags); // 根據給定的flags標誌位,將流s中對應的條件狀態復位,flags的型別為iostate
s.setstate(flags); // 根據給定的flags標誌位,將流s中對應的條件狀態置位。,flags的型別為iostate
s.rdstate(); // 返回一個iostate型別的值,對應流當前的狀態。
註釋:Windows下標準輸入輸入檔案結束符為Ctrl+z,Linux為Ctrl+d。
最後就是,我們來完成一下《c++ primer 第五版》的281那個練習題,程式碼如下:
#include<iostream>
#include<string>
using namespace std;
istream & test(istream & s)
{
string str;
while ((s >> str).eof() == false)
{
cout << str << " " << endl;
}
cout << "結束" << endl;
s.clear();
return s;
}
int main()
{
//Windows下標準輸入輸入檔案結束符為Ctrl+z,Linux為Ctrl+d。
test(cin);
system("pause");
return 0;
}
測試結果:
緩衝區詳解
(1)輸入緩衝區
觀察如下程式碼:
#include<iostream>
using namespace std;
int main()
{
char ch;
while (cin >> ch)
{
cout << ch;
}
system("pause");
return 0;
}
我們預想的是,我們輸入一個字元就顯示一個字元,但是實際上情況是如下圖:
其實,輸入字元立即回顯是非緩衝或直接輸入的一個形式,它表示你所鍵入的字元對正在等待的程式立即變為可用。相反,延遲迴顯是緩衝輸入的例子,這種情況下你所鍵入的字元塊被收集並存儲在一個被稱為緩衝區的臨時儲存區域中。按下回車鍵可使你輸入的字元段對程式起作用。
緩衝輸入一般常用在文字程式內,當你輸入有錯誤時,就可以使用你的鍵盤更正修正錯誤。當最終按下回車鍵時,你就可以傳送正確的輸入。
而在一些互動性的遊戲裡需要非緩衝輸入,如:遊戲裡你按下一個鍵時就要執行某個命令。
輸入緩衝分類:
完全緩衝:緩衝區被充滿時被清空(內容傳送到其目的地)。這種型別的緩衝通常出現在檔案輸入中。
行緩衝:遇到一個換行字元時被清空緩衝區。鍵盤的輸入是標準的行緩衝,因此按下回車鍵將清空緩衝區
(2)輸出緩衝區
每個輸出流都會管理一個緩衝區,用了儲存程式讀寫的資料,這個和輸入緩衝區是一個道理的,但是輸出不一樣,我們是希望在程式結束是,輸出緩衝區中的內容都要被輸出去,所以會有緩衝區重新整理,在下面這幾種情況會引起緩衝區的重新整理(注意:如要程式異常終止,輸出緩衝區是不會被重新整理的。當一個程式崩潰後,它所輸出的資料很可能停留在輸出緩衝區中等待列印。所以最好在每個輸出後加一個緩衝區重新整理的操作):
程式正常結束,作為main函式的return操作的一部分,緩衝重新整理被執行。
緩衝區滿時,需要重新整理緩衝,而後新的資料才能繼續寫入緩衝區。
我們可以使用操縱符endl來顯式重新整理緩衝區。
在每個輸出之後,我們可以用操縱符unitbuf設定流的內部狀態,來清空緩衝區。預設情況下,對cerr是設定unitbuf的,因此寫到cerr的內容都是立即重新整理的。
一個輸出流被關聯到另一個流。在這種情況下,當讀寫被關聯的流時,關聯到的流的緩衝區會被重新整理,cin和cerr都關聯到cout。因此讀cin或寫cerr會導致cout的緩衝區被重新整理(cin立即回顯的一個原因)。
IO庫中除了endl可以重新整理緩衝區外,ends和flush也會重新整理緩衝區。只是它們會有一點差別:
cout << "hi!" << endl; // 輸出 hi 和一個換行符,然後重新整理緩衝區
cout << "hi!" << flush; // 輸出hi,然後重新整理緩衝區,不附加任何額外字元
cout << "hi!" << ends; // 輸出hi和一個空字元。然後重新整理緩衝區
unitbuf操作符,如果我們希望每次輸出操作後都要重新整理緩衝區,那麼就可以使用:unitbuf,它就是告訴系統,以後每次進行輸出操作後都要指向flush操作,我們之前提到的cerr就是設定了unitbuf的
cout << unitbuf; // 所有輸出操作後都立即重新整理緩衝區
// 任何輸出都立即重新整理,無緩衝
cout << nounitbuf; // 回到正常的緩衝方式