在前一節中我們有幾個例子中提到了cin函數出錯,以致不再執行讀操作(程式8)。而且我們經常會看到程式中會出現cin.clear(),cin.ignore(), cin.fail()等函數。這些函數都是與cin的錯誤處理有關的。這一節我們來分析一下cin的錯誤處理機制,並且學習幾個重要的函數:cin.fail(), cin.bad(), cin.good(), cin.clear(), cin.ignore()等。
程式執行時有一個標誌變數來標誌輸入的異常狀態,其中有三位標誌位分別用來標誌三種異常資訊,他們分別是:failbit,eofbit,badbit。這三個標誌位在標誌變數中是這樣分配的:
____________________________________
| 2 | 1 | 0 |
| failbit | eofbit | badbit |
|___________|__________|___________|
看一下這幾個標誌位的作用(引用msdn):
badbit, to record a loss of integrity of the stream buffer.
eofbit, to record end-of-file while extracting from a stream.
failbit, to record a failure to extract a valid field from a stream.
In addition, a useful value is goodbit, where no bits are set.
接下來我麼看幾個ios類的資料定義(引用msdn):
typedef T2 iostate;
static const iostate badbit, eofbit, failbit, goodbit;
這裡ios類定義了這四個常量badbit, eofbit, failbit, goodbit,其實這四個標誌常量就是取對應標誌位的掩碼,也即輸入的四種異常情況!
以上四個常量對應的取值為:
ios::badbit 001 輸入(輸出)流出現致命錯誤,不可挽回
ios::eofbit 010 已經到達檔案尾
ios::failbit 100 輸入(輸出)流出現非致命錯誤,可挽回
ios::goodbit 000 流狀態完全正常, 各異常標誌位都為0
我們可以用輸出語句來驗證這幾個常量的值:
cout << ios:: failbit << endl;
cout << ios:: eofbit << endl;
cout << ios:: badbit << endl;
cout << ios:: goodbit << endl;
輸出的結果為:
4
2
1
0
【注意】它們不是failbit、badbit、eofbit、goodbit這四個標記位的存貯變數,而是四個標誌四種異常狀態的常量,其實他們就相當於取對應狀態標誌位的掩碼。如果標誌變數為flag,則flag & failbit 就取得fail標誌位。
搞清楚了標誌位的原理後,我們來看幾個關於異常標誌的函數:
1、iostate ios::rdstate()
取標誌變數的值,我們可以用該函數取得整個標誌變數的值,再與前面定義的標誌位常量相與就可以獲得對應標誌位的狀態。如:
void TestFlags( ios& x ) // 獲得x流的三個標誌位狀態
{
cout << ( x.rdstate( ) & ios::badbit ) << endl;
cout << ( x.rdstate( ) & ios::failbit ) << endl;
cout << ( x.rdstate( ) & ios::eofbit ) << endl;
cout << endl;
}
2、bool ios::fail() const;
1 or true if rdstate & failbit is nonzero, otherwise 0 or false. (引用msdn)
其中rdstate即通過rdstate()取得的標識變數的值,與failbit相與,即取得failbit標誌位的值,如果結果非零則放回true,否則返回false。即該函數返回failbit的狀態,將標誌位狀態通過bool值返回。
3、bool ios::bad() const;
1 or true if rdstate & badbit is nonzero; otherwise 0. (引用msdn)
與fail()相似。
4、bool ios::good() const;
1 or true if rdstate == goodbit (no state flags are set), otherwise, 0 or false. (引用msdn)
改函數取goodbit的情況,即三個標誌位都0(即沒有任何異常情況)時返回true,否則返回false。
5、void ios::clear(iostate _State=goodbit);
該函數用來重設標識變數,_State是用來重設的值,預設為goodbit,即預設時將所有標誌位清零。使用者也可以傳進參數,如:clear(failbit),這樣就將標識變數置為failbit(即:001)。
我們一般是用它的預設值,當cin出現異常,我們用該函數將所有標誌位重設。如果cin出現異常,沒有重設標誌的話沒法執行下一次的cin操作。如上一節的程式2的測試二為什麼第二次輸入操作沒有執行?程式8中 cin>>ch 為什麼沒有執行?都是這個原因!!!
所以經常在程式中使用 cin.clear(), 為了重設錯誤標誌!
6、另外還有一個函數 void ios::setstate(iostate _State);
這個函數也是用來設定標識變數的,但與clear()不同。clear()是將所有標誌清零,在置以參數新的標誌。而該函數不清零其他的標誌,而只是將參數對應的標誌位置位。這個函數不是經常使用,這裡不再贅述。
在搞清楚了這幾個函數後,對cin輸入操作的錯誤處理就有了比較深的瞭解了。下面我們回過頭來看看上一節程式8的測試,因為第一次用getline()讀取字串超長,所以導致出現異常,大家可以查看一下標誌位來驗證一下!所以會導致後面的 cin>>ch 語句沒有執行。那我們利用前面學習的clear()函數來強制重設錯誤標誌,看看會出現什麼情況呢?
程式9:
#include <iostream>
using namespace std;
int main ()
{
char ch, str[20];
cin.getline(str, 5);
cout<<"flag1:"<<cin.good()<<endl; // 查看goodbit狀態,即是否有異常
cin.clear(); // 清除錯誤標誌
cout<<"flag1:"<<cin.good()<<endl; // 清除標誌後再查看異常狀態
cin>>ch;
cout<<"str:"<<str<<endl;
cout<<"ch :"<<ch<<endl;
return 0;
}
測試輸入:
12345[Enter]
輸出:
flag1:0 // good()返回false說明有異常
flag2:1 // good()返回true說明,clear()已經清除了錯誤標誌
str:1234
ch :5
【分析】程式執行結束還是只執行了一次讀操作,cin>>ch還是沒有從鍵盤讀取資料,但是與程式8中不同,這裡列印了ch的值為'5',而且在cin>>ch之前已經清楚了錯誤標誌,也就是cin>>ch的讀操作實際上執行了。這就是前面講的cin讀取資料的原理:它是直接從輸入緩衝區中取資料的。此例中,第一次輸入"12345", 而getline(str, 5)根據參數'5'只取緩衝區中的前4個字元,所以str取的是"1234",而字元'5'仍在緩衝區中,所以cin>>ch直接從緩衝區中取得資料,沒有從鍵盤讀取資料!
也就是當前一次讀取資料出錯後,如果緩衝區沒有清空的話,重設錯誤標誌還不夠!要是能將緩衝區的殘留資料清空了就好了哦!下面我們再來看一個很重要的函數!
7、basic_istream& ignore(streamsize _Count = 1, int_type _Delim = traits_type::eof());
function: Causes a number of elements to be skipped from the current read position.
Parameters:
_Count, The number of elements to skip from the current read position.
_Delim, The element that, if encountered before count, causes ignore to return and allowing all elements after _Delim to be read. (引用msdn)
這個函數用來丟棄輸入緩衝區中的字元,第一參數定義一個數,第二個參數定義一個字元變數。下面解釋一下函數是怎樣執行的:函數不停的從緩衝區中取一個字元,並判斷是不是_Delim,如果不是則丟棄並進行計數,當計數達到_Count退出,如果是則丟棄字元退出。例:cin.ignore(5, 'a'); 函數將不斷從緩衝區中取一個字元丟棄,直到丟棄的字元數達到5或者讀取的字元為'a'。下面我們看個程式例子:
程式10:
#include <iostream>
using namespace std;
int main ()
{
cin.ignore(5, 'a');
return 0;
}
測試一輸入:
c[enter]
c[enter]
c[enter]
c[enter]
c[enter]
程式結束。
【分析】程式開始時緩衝區是空的,cin.ignore()到緩衝區中取資料,沒有則請求從鍵盤輸入,每次從鍵盤輸入一個字元,如果不是'a'則丟棄,所以該測試中共輸入了5次,直到計數達到5。
測試二輸入:
c[enter]
c[enter]
a[enter]
程式結束。
【分析】前面兩個字元不是'a'丟棄且計數沒達到5,第三次輸入為'a', 丟棄該字元程式結束!
丟棄一個字元:
我們看看這個函數的預設值,第一個參數預設為1,第二個參數預設為EOF。所以cin.ignore()就是丟棄緩衝區中的第一個字元,這在程式中也是比較常用的!我們回過頭看看程式5,程式5中用cin.get()讀取字元,第一次讀取時用斷行符號符結束,而get函數不丟棄斷行符號符,所以斷行符號符仍殘留在緩衝區中,導致第二次讀取資料直接從緩衝區中取得斷行符號符!這與我們最初的用以是不相符的,既然cin.get()不會自動丟棄輸入結束時的斷行符號符,這裡我們學會了ignore()函數,我們就可以自己手動求其斷行符號符啊!所以程式5可以這樣改動:
程式11:
#include <iostream>
using namespace std;
int main()
{
char c1, c2;
cin.get(c1);
cin.ignore(); // 用該函數的預設情況,丟棄一個字元,即上次輸入結束的斷行符號符
cin.get(c2);
cout<<c1<<" "<<c2<<endl; // 列印兩個字元
cout<<(int)c1<<" "<<(int)c2<<endl; // 列印這兩個字元的ASCII值
return 0;
}
測試一輸入:
a[Enter]
b[Enter]
輸出:
a
b
97 98
【分析】這樣程式就正常了!
清空整個緩衝區:
其實該函數最常用的方式是這樣的,將第一個參數設的非常大,將第二個參數設為'\n',這樣就可以緩衝區中斷行符號符中的所有殘留資料,因為一般情況下前面輸入殘留的資料是沒有用的,所以在進行新一次輸入操作前將緩衝區中所有資料清空是比較合理。
如:cin.ignore(1024, '\n');
或者:cin.ignore(std::numeric_limits<std::streamsize>::max(), '\n');