在閱讀別人開發的專案中,也許你會經常看到了多處使用異常的程式碼,也許你也很少遇見使用異常處理的程式碼。那在什麼時候該使用異常,又在什麼時候不該使用異常呢?在學習完異常基本概念和語法之後,後面會有講解。
(1)異常丟擲和捕捉語句
//1.丟擲異常
throw 異常物件
//2.異常捕捉
try{
可能會發生異常的程式碼
}catch(異常物件){
異常處理程式碼
}
- throw子句:throw 子句用於丟擲異常,被丟擲的異常可以是C++的內建型別(例如: throw int(1);),也可以是自定義型別。
- try區段:這個區段中包含了可能發生異常的程式碼,在發生了異常之後,需要通過throw丟擲。
- catch子句:每個catch子句都代表著一種異常的處理。catch子句用於處理特定型別的異常。catch塊的引數推薦採用地址傳遞而不是值傳遞,不僅可以提高效率,還可以利用物件的多型性。
(2)異常的處理規則
- throw丟擲的異常型別與catch抓取的異常型別要一致;
- throw丟擲的異常型別可以是子類物件,catch可以是父類物件;
- catch塊的引數推薦採用地址傳遞而不是值傳遞,不僅可以提高效率,還可以利用物件的多型性。另外,派生類的異常捕獲要放到父類異常撲獲的前面,否則,派生類的異常無法被撲獲;
- 如果使用catch引數中,使用基類捕獲派生類物件,一定要使用傳遞引用的方式,例如catch (exception &e);
- 異常是通過丟擲物件而引發的,該物件的型別決定了應該啟用哪個處理程式碼;
- 被選中的處理程式碼是呼叫鏈中與該物件型別匹配且離丟擲異常位置最近的那一個;
- 在try的語句塊內宣告的變數在外部是不可以訪問的,即使是在catch子句內也不可以訪問;
- 棧展開會沿著巢狀函式的呼叫鏈不斷查詢,直到找到了已丟擲的異常匹配的catch子句。如果丟擲的異常一直沒有函式捕獲(catch),則會一直上傳到c++執行系統那裡,導致整個程式的終止。
(3)例項
- 例項1:丟擲自定義型別異常。
class Data
{
public:
Data() {}
};
void fun(int n)
{
if(n==0)
throw 0;//拋異常 int異常
if(n==1)
throw "error"; //拋字串異常
if(n==2)
{
Data data;
throw data;
}
if(n>3)
{
throw 1.0;
}
}
int main()
{
try {
fun(6);//當異常發生fun裡面,fun以下程式碼就不會再執行,調到catch處執行異常處理程式碼,後繼續執行catch以外的程式碼。當throw丟擲異常後,沒有catch捕捉,則整個程式會退出,不會執行整個程式的以下程式碼
cout<<"*************"<<endl;
}catch (int i) {
cout<<i<<endl;
}catch (const char *ptr)
{
cout<<ptr<<endl;
}catch(Data &d)
{
cout<<"data"<<endl;
}catch(...)//抓取 前面異常以外的所有其他異常
{
cout<<"all"<<endl;
}
return 0;
}
- 例項2:標準出錯類丟擲和捕捉異常。
#include <iostream>
using namespace std;
int main()
{
try {
char* p = new char[0x7fffffff]; //丟擲異常
}
catch (exception &e){
cout << e.what() << endl; //捕獲異常,然後程式結束
}
return 0;
}
輸出結果:
當使用new進行開空間時,申請記憶體失敗,系統就會丟擲異常,不用使用者自定義異常型別,此時捕獲到異常時,就可告訴使用者是哪裡的錯誤,便於修改。
- 例項3:繼承標準出錯類的派生類的異常丟擲和捕捉。
#include <iostream>
#include <exception>
#include <sys/types.h>
#include <sys/stat.h>
#include <fcntl.h>
using namespace std;
class FileException :public exception
{
public:
FileException(string msg) {
this->exStr = msg;
}
virtual const char*what() const noexcept//宣告這個函式不能再拋異常
{
return this->exStr.c_str();
}
protected:
string exStr;
};
void fun()
{
int fd = ::open("./open.txt",O_RDWR);
if(fd<0)
{
FileException openFail("open fail"); //建立異常物件
throw openFail;//拋異常
}
}
int main( )
{
try {
fun();
} catch (exception &e) {//一般需要使用引用
cout<<e.what()<<endl;
}
cout<<"end"<<endl;
return 0;
}
當檔案不存在時,輸出結果:
如果在Linux上執行,上述程式碼需要根據環境修改:
98標準寫法
~FileException()throw(){}//必須要
virtual const char*what() const throw()//宣告這個函式不能再拋異常
{
return this->exStr.c_str();
}
//編譯
g++ main.cpp
2011標準寫法
~FileException()noexcept{}//必須要
virtual const char*what() const noexcept//宣告這個函式不能再拋異常
{
return this->exStr.c_str();
}
//編譯
g++ main.cpp -std=c++11 指定用c++11標準編譯
(4)總結
1. 使用異常處理的優點:
- 傳統錯誤處理技術,檢查到一個錯誤,只會返回退出碼或者終止程式等等,我們只知道有錯誤,但不能更清楚知道是哪種錯誤。使用異常,把錯誤和處理分開來,由庫函式丟擲異常,由呼叫者捕獲這個異常,呼叫者就可以知道程式函式庫呼叫出現的錯誤是什麼錯誤,並去處理,而是否終止程式就把握在呼叫者手裡了。
2. 使用異常的缺點:
- 如果使用異常,光憑檢視程式碼是很難評估程式的控制流:函式返回點可能在你意料之外,這就導致了程式碼管理和除錯的困難。啟動異常使得生成的二進位制檔案體積變大,延長了編譯時間,還可能會增加地址空間的壓力。
- C++沒有垃圾回收機制,資源需要自己管理。有了異常非常容易導致記憶體洩漏、死鎖等異常安全問題。 這個需要使用RAII來處理資源的管理問題。學習成本較高。
- C++標準庫的異常體系定義得不好,導致大家各自定義各自的異常體系,非常的混亂。
3. 什麼時候使用異常?
- 建議:除非已有的專案或底層庫中使用了異常,要不然儘量不要使用異常,雖然提供了方便,但是開銷也大。
4. 程式所有的異常都可以catch到嗎?
- 並非如此,只有發生異常,並且又丟擲異常的情況才能被catch到。例如,陣列下標訪問越界的情況,系統是不會自身丟擲異常的,所以我們無論怎麼catch都是無效的;在這種情況,我們需要自定義丟擲型別,判斷陣列下標是否越界,然後再根據自身需要throw自定義異常物件,這樣才可以catch到異常,並進行進一步處理。