溫馨提示×

溫馨提示×

您好,登錄后才能下訂單哦!

密碼登錄×
登錄注冊×
其他方式登錄
點擊 登錄注冊 即表示同意《億速云用戶服務(wù)條款》

C++中的異常處理機制詳解

發(fā)布時間:2020-09-03 10:24:32 來源:腳本之家 閱讀:159 作者:hebedich 欄目:編程語言

異常處理

增強錯誤恢復(fù)能力是提高代碼健壯性的最有力的途徑之一,C語言中采用的錯誤處理方法被認為是緊耦合的,函數(shù)的使用者必須在非??拷瘮?shù)調(diào)用的地方編寫錯誤處理代碼,這樣會使得其變得笨拙和難以使用。C++中引入了異常處理機制,這是C++的主要特征之一,是考慮問題和處理錯誤的一種更好的方式。使用錯誤處理可以帶來一些優(yōu)點,如下:

錯誤處理代碼的編寫不再冗長乏味,并且不再和正常的代碼混合在一起,程序員只需要編寫希望產(chǎn)生的代碼,然后在后面某個單獨的區(qū)段里編寫處理錯誤的嗲嗎。多次調(diào)用同一個函數(shù),則只需要某個地方編寫一次錯誤處理代碼。

錯誤不能被忽略,如果一個函數(shù)必須向調(diào)用者發(fā)送一次錯誤信息。它將拋出一個描述這個錯誤的對象。

傳統(tǒng)的錯誤處理和異常處理

在討論異常處理之前,我們先談?wù)凜語言中的傳統(tǒng)錯誤處理方法,這里列舉了如下三種:

在函數(shù)中返回錯誤,函數(shù)會設(shè)置一個全局的錯誤狀態(tài)標志。

使用信號來做信號處理系統(tǒng),在函數(shù)中raise信號,通過signal來設(shè)置信號處理函數(shù),這種方式耦合度非常高,而且不同的庫產(chǎn)生的信號值可能會發(fā)生沖突

使用標準C庫中的非局部跳轉(zhuǎn)函數(shù) setjmp和longjmp ,這里使用setjmp和longjmp來演示下如何進行錯誤處理:

#include 
#include 
jmp_buf static_buf; //用來存放處理器上下文,用于跳轉(zhuǎn)
void do_jmp()
 {
 //do something,simetime occurs a little error
 //調(diào)用longjmp后,會載入static_buf的處理器信息,然后第二個參數(shù)作為返回點的setjmp這個函數(shù)的返回值
 longjmp(static_buf,10);//10是錯誤碼,根據(jù)這個錯誤碼來進行相應(yīng)的處理
 }
int main()
 {
 int ret = 0;
 //將處理器信息保存到static_buf中,并返回0,相當于在這里做了一個標記,后面可以跳轉(zhuǎn)過來
 if((ret = setjmp(static_buf)) == 0) {
 //要執(zhí)行的代碼
 do_jmp();
 } else { //出現(xiàn)了錯誤
 if (ret == 10)
 std::cout << "a little error" << std::endl;
 }
 }

錯誤處理方式看起來耦合度不是很高,正常代碼和錯誤處理的代碼分離了,處理處理的代碼都匯聚在一起了。但是基于這種局部跳轉(zhuǎn)的方式來處理代碼,在C++中卻存在很嚴重的問題,那就是對象不能被析構(gòu),局部跳轉(zhuǎn)后不會主動去調(diào)用已經(jīng)實例化對象的析構(gòu)函數(shù)。這將導致內(nèi)存泄露的問題。下面這個例子充分顯示了這點

#include 
#include 
using namespace std;
class base {
 public:
 base() {
 cout << "base construct func call" << endl;
 }
 ~base() {
 cout << "~base destruct func call" << endl;
 }
 };
jmp_buf static_buf;
void test_base() {
 base b;
 //do something
 longjmp(static_buf,47);//進行了跳轉(zhuǎn),跳轉(zhuǎn)后會發(fā)現(xiàn)b無法析構(gòu)了
 }
int main() {
 if(setjmp(static_buf) == 0) {
 cout << "deal with some thing" << endl;
 test_base();
 } else {
 cout << "catch a error" << endl;
 }
 }

在上面這段代碼中,只有base類的構(gòu)造函數(shù)會被調(diào)用,當longjmp發(fā)生了跳轉(zhuǎn)后,b這個實例將不會被析構(gòu)掉,但是執(zhí)行流已經(jīng)無法回到這里,b這個實例將不會被析構(gòu)。這就是局部跳轉(zhuǎn)用在C++中來處理錯誤的時候帶來的一些問題,在C++中異常則不會有這些問題的存在。那么接下來看看如何定義一個異常,以及如何拋出一個異常和捕獲異常吧.

異常的拋出

class MyError {
 const char* const data;
public:
 MyError(const char* const msg = 0):data(msg)
 {
 //idle
 }
};
void do_error() {
 throw MyError("something bad happend");
 }
int main()
 {
 do_error();
 }

上面的例子中,通過throw拋出了一個異常類的實例,這個異常類,可以是任何一個自定義的類,通過實例化傳入的參數(shù)可以表明發(fā)生的錯誤信息。其實異常就是一個帶有異常信息的類而已。異常被拋出后,需要被捕獲,從而可以從錯誤中進行恢復(fù),那么接下來看看如何去捕獲一個異常吧。在上面這個例子中使用拋出異常的方式來進行錯誤處理相比與之前使用局部跳轉(zhuǎn)的實現(xiàn)來說,最大的不同之處就是異常拋出的代碼塊中,對象會被析構(gòu),稱之為堆棧反解.

異常的捕獲

C++中通過catch關(guān)鍵字來捕獲異常,捕獲異常后可以對異常進行處理,這個處理的語句塊稱為異常處理器。下面是一個簡單的捕獲異常的例子:

try{
    //do something
    throw string("this is exception");
  } catch(const string& e) {
    cout << "catch a exception " << e << endl;
  }

catch有點像函數(shù),可以有一個參數(shù),throw拋出的異常對象,將會作為參數(shù)傳遞給匹配到到catch,然后進入異常處理器,上面的代碼僅僅是展示了拋出一種異常的情況,加入try語句塊中有可能會拋出多種異常的,那么該如何處理呢,這里是可以接多個catch語句塊的,這將導致引入另外一個問題,那就是如何進行匹配。

異常的匹配

異常的匹配我認為是符合函數(shù)參數(shù)匹配的原則的,但是又有些不同,函數(shù)匹配的時候存在類型轉(zhuǎn)換,但是異常則不然,在匹配過程中不會做類型的轉(zhuǎn)換,下面的例子說明了這個事實:

#include 
using namespace std;
 int main()
 {
 try{
 throw 'a';
 }catch(int a) {
 cout << "int" << endl;
 }catch(char c) {
 cout << "char" << endl;
 }
 }

上面的代碼的輸出結(jié)果是char,因為拋出的異常類型就是char,所以就匹配到了第二個異常處理器??梢园l(fā)現(xiàn)在匹配過程中沒有發(fā)生類型的轉(zhuǎn)換。將char轉(zhuǎn)換為int。盡管異常處理器不做類型轉(zhuǎn)換,但是基類可以匹配到派生類這個在函數(shù)和異常匹配中都是有效的,但是需要注意catch的形參需要是引用類型或者是指針類型,否則會 導致切割派生類這個問題。

//基類
class Base{
 public:
 Base(string msg):m_msg(msg)
 {
 }
 virtual void what(){
 cout << m_msg << endl;
 }
 void test()
 {
 cout << "I am a CBase" << endl;
 }
 protected:
 string m_msg;
};
//派生類,重新實現(xiàn)了虛函數(shù)
class CBase : public Base
{
 public:
 CBase(string msg):Base(msg)
 {
 }
 void what()
 {
 cout << "CBase:" << m_msg << endl;
 }
 };
int main()
 {
 try {
 //do some thing
 //拋出派生類對象
 throw CBase("I am a CBase exception");
 }catch(Base& e) { //使用基類可以接收
 e.what();
 }
 }

上面的這段代碼可以正常的工作,實際上我們?nèi)粘>帉懽约旱漠惓L幚砗瘮?shù)的時候也是通過繼承標準異常來實現(xiàn)字節(jié)的自定義異常的,但是如果將Base&換成Base的話,將會導致對象被切割,例如下面這段代碼將會編譯出錯,因為CBase被切割了,導致CBase中的test函數(shù)無法被調(diào)用。

try {
    //do some thing
    throw CBase("I am a CBase exception");
}catch(Base e) {

e.test();

}

到此為此,異常的匹配算是說清楚了,總結(jié)一下,異常匹配的時候基本上遵循下面幾條規(guī)則:

異常匹配除了必須要是嚴格的類型匹配外,還支持下面幾個類型轉(zhuǎn)換.

允許非常量到常量的類型轉(zhuǎn)換,也就是說可以拋出一個非常量類型,然后使用catch捕捉對應(yīng)的常量類型版本

允許從派生類到基類的類型轉(zhuǎn)換

允許數(shù)組被轉(zhuǎn)換為數(shù)組指針,允許函數(shù)被轉(zhuǎn)換為函數(shù)指針

假想一種情況,當我要實現(xiàn)一代代碼的時候,希望無論拋出什么類型的異常我都可以捕捉到,目前來說我們只能寫上一大堆的catch語句捕獲所有可能在代碼中出現(xiàn)的異常來解決這個問題,很顯然這樣處理起來太過繁瑣,幸好C++提供了一種可以捕捉任何異常的機制,可以使用下列代碼中的語法。

catch(...) {
    //異常處理器,這里可以捕捉任何異常,帶來的問題就是無法或者異常信息
   }
如果你要實現(xiàn)一個函數(shù)庫,你捕捉了你的函數(shù)庫中的一些異常,但是你只是記錄日志,并不去處理這些異常,處理異常的事情會交給上層調(diào)用的代碼來處理.對于這樣的一個場景C++也提供了支持.

try{
    throw Exception("I am a exception");  
  }catch(...) {
    //log the exception
    throw;
  }

通過在catch語句塊中加入一個throw,就可以把當前捕獲到的異常重新拋出.在異常拋出的那一節(jié)中,我在代碼中拋出了一個異常,但是我沒有使用任何catch語句來捕獲我拋出的這個異常,執(zhí)行上面的程序會出現(xiàn)下面的結(jié)果.

terminate called after throwing an instance of 'MyError'
Aborted (core dumped)

為什么會出現(xiàn)這樣的結(jié)果呢?,當我們拋出一個異常的時候,異常會隨著函數(shù)調(diào)用關(guān)系,一級一級向上拋出,直到被捕獲才會停止,如果最終沒有被捕獲將會導致調(diào)用terminate函數(shù),上面的輸出就是自動調(diào)用terminate函數(shù)導致的,為了保證更大的靈活性,C++提供了set_terminate函數(shù)可以用來設(shè)置自己的terminate函數(shù).設(shè)置完成后,拋出的異常如果沒有被捕獲就會被自定義的terminate函數(shù)進行處理.下面是一個使用的例子:

#include 
#include 
#include 
using namespace std;
class MyError {
 const char* const data;
 public:
 MyError(const char* const msg = 0):data(msg)
 {
 //idle
 }
 };
void do_error() {
 throw MyError("something bad happend");
 }
 //自定義的terminate函數(shù),函數(shù)原型需要一致
 void terminator()
 {
 cout << "I'll be back" << endl;
 exit(0);
 }
int main()
 {
 //設(shè)置自定義的terminate,返回的是原有的terminate函數(shù)指針
 void (*old_terminate)() = set_terminate(terminator);
 do_error();
 }

 上面的代碼會輸出I'll be back
到此為此關(guān)于異常匹配的我所知道的知識點都已經(jīng)介紹完畢了,那么接著可以看看下一個話題,異常中的資源清理.

異常中的資源清理

在談到局部跳轉(zhuǎn)的時候,說到局部調(diào)轉(zhuǎn)不會調(diào)用對象的析構(gòu)函數(shù),會導致內(nèi)存泄露的問題,C++中的異常則不會有這個問題,C++中通過堆棧反解將已經(jīng)定義的對象進行析構(gòu),但是有一個例外就是構(gòu)造函數(shù)中如果出現(xiàn)了異常,那么這會導致已經(jīng)分配的資源無法回收,下面是一個構(gòu)造函數(shù)拋出異常的例子:

#include 
#include 
using namespace std;
class base
 {
 public:
 base()
 {
 cout << "I start to construct" << endl;
 if (count == 3) //構(gòu)造第四個的時候拋出異常
 throw string("I am a error");
 count++;
 }
 ~base()
 {
 cout << "I will destruct " << endl;
 }
 private:
 static int count;
 };
int base::count = 0;
int main()
 {
 try{
 base test[5];
 } catch(...){
 cout << "catch some error" << endl;
 }
 }

 上面的代碼輸出結(jié)果是:

 I start to construct
 I start to construct
 I start to construct
 I start to construct
 I will destruct 
 I will destruct 
 I will destruct 
 catch some error

在上面的代碼中構(gòu)造函數(shù)發(fā)生了異常,導致對應(yīng)的析構(gòu)函數(shù)沒有執(zhí)行,因此實際編程過程中應(yīng)該避免在構(gòu)造函數(shù)中拋出異常,如果沒有辦法避免,那么一定要在構(gòu)造函數(shù)中對其進行捕獲進行處理.最后介紹一個知識點就是函數(shù)try語句塊,如果main函數(shù)可能會拋出異常該怎么捕獲?,如果構(gòu)造函數(shù)中的初始化列表可能會拋出異常該怎么捕獲?下面的兩個例子說明了函數(shù)try語句塊的用法:

#include 
using namespace std;
int main() try {
 throw "main";
 } catch(const char* msg) {
 cout << msg << endl;
 return 1;
 }

 main函數(shù)語句塊,可以捕獲main函數(shù)中拋出的異常.

 class Base
 {
 public:
 Base(int data,string str)try:m_int(data),m_string(str)//對初始化列表中可能會出現(xiàn)的異常也會進行捕捉
 {
 // some initialize opt
 }catch(const char* msg) {
 cout << "catch a exception" << msg << endl;
 }
 private:
 int m_int;
 string m_string;
 };
int main()
 {
 Base base(1,"zhangyifei");
 }

上面說了很多都是關(guān)于異常的使用,如何定義自己的異常,編寫異常是否應(yīng)該遵循一定的標準,在哪里使用異常,異常是否安全等等一系列的問題,下面會一一討論的.

標準異常

C++標準庫給我們提供了一系列的標準異常,這些標準異常都是從exception類派生而來,主要分為兩大派生類,一類是logic_error,另一類則是runtime_error這兩個類在stdexcept頭文件中,前者主要是描述程序中出現(xiàn)的邏輯錯誤,例如傳遞了無效的參數(shù),后者指的是那些無法預(yù)料的事件所造成的錯誤,例如硬件故障或內(nèi)存耗盡等,這兩者都提供了一個參數(shù)類型為std::string的構(gòu)造函數(shù),這樣就可以將異常信息保存起來,然后通過what成員函數(shù)得到異常信息.

#include 
#include 
#include 
using namespace std;
class MyError:public runtime_error {
 public:
 MyError(const string& msg = "") : runtime_error(msg) {}
};
//runtime_error logic_error 兩個都是繼承自標準異常,帶有string構(gòu)造函數(shù)
 //
 int main()
 {
 try {
 throw MyError("my message"); 
 } catch(MyError& x) {
 cout << x.what() << endl; 
 }
 }

異常規(guī)格說明

假設(shè)一個項目中使用了一些第三方的庫,那么第三方庫中的一些函數(shù)可能會拋出異常,但是我們不清楚,那么C++提供了一個語法,將一個函數(shù)可能會拋出的異常列出來,這樣我們在編寫代碼的時候參考函數(shù)的異常說明即可,但是C++11中這中異常規(guī)格說明的方案已經(jīng)被取消了,所以我不打算過多介紹,通過一個例子看看其基本用法即可,重點看看C++11中提供的異常說明方案:

#include 
#include 
#include 
#include 
using namespace std;
class Up{};
 class Fit{};
 void g();
 //異常規(guī)格說明,f函數(shù)只能拋出Up 和Fit類型的異常
 void f(int i)throw(Up,Fit) {
 switch(i) {
 case 1: throw Up();
 case 2: throw Fit(); 
 }
 g();
 }
void g() {throw 47;}
void my_ternminate() {
 cout << "I am a ternminate" << endl;
 exit(0);
 }
void my_unexpected() {
 cout << "unexpected exception thrown" << endl;
 // throw Up();
 throw 8;
 //如果在unexpected中繼續(xù)拋出異常,拋出的是規(guī)格說明中的 則會被捕捉程序繼續(xù)執(zhí)行
 //如果拋出的異常不在異常規(guī)格說明中分兩種情況
 //1.異常規(guī)格說明中有bad_exception ,那么會導致拋出一個bad_exception
 //2.異常規(guī)格說明中沒有bad_exception 那么會導致程序調(diào)用ternminate函數(shù)
 // exit(0);
 }
int main() {
 set_terminate(my_ternminate);
 set_unexpected(my_unexpected);
 for(int i = 1;i <=3;i++)
 {
 //當拋出的異常,并不是異常規(guī)格說明中的異常時
 //會導致最終調(diào)用系統(tǒng)的unexpected函數(shù),通過set_unexpected可以
 //用來設(shè)置自己的unexpected汗函數(shù)
 try {
 f(i); 
 }catch(Up) {
 cout << "Up caught" << endl; 
 }catch(Fit) {
 cout << "Fit caught" << endl; 
 }catch(bad_exception) {
 cout << "bad exception" << endl; 
 }
 }
 }
 }

上面的代碼說明了異常規(guī)格說明的基本語法,以及unexpected函數(shù)的作用,以及如何自定義自己的unexpected函數(shù),還討論了在unexpected函數(shù)中繼續(xù)拋出異常的情況下,該如何處理拋出的異常.C++11中取消了這種異常規(guī)格說明.引入了一個noexcept函數(shù),用于表明這個函數(shù)是否會拋出異常

void recoup(int) noexecpt(true); //recoup不會拋出異常
void recoup(int) noexecpt(false); //recoup可能會拋出異常

此外還提供了noexecpt用來檢測一個函數(shù)是否不拋出異常.

異常安全

異常安全我覺得是一個挺復(fù)雜的點,不光光需要實現(xiàn)函數(shù)的功能,還要保存函數(shù)不會在拋出異常的情況下,出現(xiàn)不一致的狀態(tài).這里舉一個例子,大家在實現(xiàn)堆棧的時候經(jīng)??吹綍械睦佣际嵌x了一個top函數(shù)用來獲得棧頂元素,還有一個返回值是void的pop函數(shù)僅僅只是把棧頂元素彈出,那么為什么沒有一個pop函數(shù)可以 即彈出棧頂元素,并且還可以獲得棧頂元素呢?

template<typename T> T stack<T>::pop()
{
  if(count == 0)
    throw logic_error("stack underflow");
  else
    return data[--count];
}

如果函數(shù)在最后一行拋出了一個異常,那么這導致了函數(shù)沒有將退棧的元素返回,但是Count已經(jīng)減1了,所以函數(shù)希望得到的棧頂元素丟失了.本質(zhì)原因是因為這個函數(shù)試圖一次做兩件事,1.返回值,2.改變堆棧的狀態(tài).最好將這兩個獨立的動作放到兩個獨立的函數(shù)中,遵守內(nèi)聚設(shè)計的原則,每一個函數(shù)只做一件事.我們 再來討論另外一個異常安全的問題,就是很常見的賦值操作符的寫法,如何保證賦值操作是異常安全的.

class Bitmap {...};
class Widget {
  ...
private:
  Bitmap *pb;
};

Widget& Widget::operator=(const Widget& rhs)

{

delete pb;

pb = new Bitmap(*rhs.pb);

return *this;

}

上面的代碼不具備自我賦值安全性,倘若rhs就是對象本身,那么將會導致*rhs.pb指向一個被刪除了的對象.那么就緒改進下.加入證同性測試.

Widget& Widget::operator=(const Widget& rhs)
{
  If(this == rhs) return *this; //證同性測試
  delete pb;
  pb = new Bitmap(*rhs.pb);
  return *this;
}

但是現(xiàn)在上面的代碼依舊不符合異常安全性,因為如果delete pb執(zhí)行完成后在執(zhí)行new Bitmap的時候出現(xiàn)了異常,則會導致最終指向一塊被刪除的內(nèi)存.現(xiàn)在只要稍微改變一下,就可以讓上面的代碼具備異常安全性.

Widget& Widget::operator=(const Widget& rhs)
{
  If(this == rhs) return *this; //證同性測試
  Bitmap *pOrig = pb;
  pb = new Bitmap(*rhs.pb); //現(xiàn)在這里即使發(fā)生了異常,也不會影響this指向的對象
  delete pOrig;
  return *this;  
}

這個例子看起來還是比較簡單的,但是用處還是很大的,對于賦值操作符來說,很多情況都是需要重載的.

向AI問一下細節(jié)

免責聲明:本站發(fā)布的內(nèi)容(圖片、視頻和文字)以原創(chuàng)、轉(zhuǎn)載和分享為主,文章觀點不代表本網(wǎng)站立場,如果涉及侵權(quán)請聯(lián)系站長郵箱:is@yisu.com進行舉報,并提供相關(guān)證據(jù),一經(jīng)查實,將立刻刪除涉嫌侵權(quán)內(nèi)容。

AI