文章目錄
- 前言
- 一、回顧C語言
- 二、異常的概念
- 三、異常的使用
- 1.異常的拋出和捕獲
- 2.異常的重新捕獲
- 三.異常安全與異常規范
- 1.異常安全
- 2.異常規范
- 四.自定義異常體系
- 五.C++標準庫的異常體系
- 六.異常優缺點
- 練習題
- 總結
前言
在本篇文章中,我們將會詳細介紹一下有關C++異常的講解,主要涉及異常的使用,應用場景,異常的優缺點等方面,同時有了異常才會引出我們之后學習的一個非常重要的知識點————智能指針。
一、回顧C語言
c語言解決錯誤時一般是通過返回錯誤碼的方式,如果遇到非常嚴重的錯誤,就會終止程序。
🌟 終止程序:比如我們遇到除零錯誤,內存錯誤,空指針的解引用等操作,我們的程序就會終止,程序終止也意味著進程的終止,這有可能會導致大量重要數據的丟失,這是非常嚴重的。
🌟 返回錯誤碼:也可以通過返回錯誤碼的方式告訴我們相應的錯誤,但這只是錯誤碼,我們還需要根據錯誤碼查找相關的錯誤信息,再進一步的分析程序,才能得出具體的錯誤信息,這時很不便利的。很多庫函數都是把錯誤信息放到error中,表示錯誤。
二、異常的概念
我們需要程序告訴是是什么錯誤。
我們C++在解決錯誤時采用的方法時異常
當函數發現了自己無法處理的錯誤時拋異常,讓函數直接或者間接的調用者處理這個異常。
我們將會引入三個關鍵字
🌟 throw:出錯時,用于拋異常(可在任意位置拋出)
🌟 try:里面放可能拋異常的代碼,其中這里的代碼稱為保護代碼,后面跟著catch
🌟 catch:在想要處理的地方,通過異常處理程序捕獲異常,可以有多個catch進行捕獲
throw…………try
{
}
catch(………)
{
}
catch(……)
{
}
其實異常就存在在我們的日常生活中,就比方說微信在網絡不好的時候,會出現一個感嘆號告訴你消息發不出去,此時的程序就是在拋異常,告訴你當前網絡狀態不佳。
如果這個異常按照C語言對錯誤的處理方式進行操作,整個微信進程就會直接崩潰,強行退出。而采用C++的拋異常機制,將拋出的異常捕獲,然后處理,比如當出現網絡不好拋異常時,微信就會采取嘗試多次發送這樣的操作,整個微信程序也不會退出。
正式由于在實際中,很多情況下我們是不希望只要產生異常就直接終止的整個進程的,通過拋異常和捕獲處理異常的手段便可讓程序保持運行。
三、異常的使用
1.異常的拋出和捕獲
異常的拋出和匹配原則
🌟異常通過拋出對象引發,這個對象與catch中()中類型進行匹配,這個對象可以是內置類型,也可以是自定義類型。
#include <iostream>
using namespace std;
double Division(int x, int y)
{//除零錯誤,拋異常if (y == 0){throw "Division by zero condition!";}else{return ((double)x / (double)y);}
}void fun()
{int a;int b;cin >> a >> b;cout << Division(a, b) << endl;
}
int main()
{try{fun();}catch (const char*errmsy){cout << errmsy << endl;}return 0;
}
我們運行運行拋異常看一下
我們確實捕捉到了,打印的就是throw拋的內容。
🌟 我們再匹配相應的catch代碼時,如果有多個都滿足,選取與throw類型匹配且距離較近的catch.
調用鏈是指函數棧幀建立的先后順序,就比如下面代碼中main函數優先建立棧幀,然后func函數建立棧幀,最后division函數建立棧幀,這樣的順序就叫調用鏈。
舉個例子說明一下
#include <iostream>
using namespace std;
double Division()
{int a;int b;cin >> a >> b;if (b == 0){throw "Division by zero condition!";}else{return ((double)a / (double)b);}
}void fun()
{try{Division();}catch (const char* errmsy){cout <<"void fun()" << errmsy << endl;}
}
int main()
{try{fun();}catch (const char* errmsy){cout <<"int main()" << errmsy << endl;}return 0;
}
main函數調用了fun函數,fun函數調用了Division函數。
main函數中catch與fun函數中的catch都與Division中的throw類型匹配,那么他會調用哪個呢??
我們拋異常來看一下
我們發現調用的是fun中的catch,因為兩個都滿足。所以找最近的那一個,就是fun函數了。
🌟拋出異常對象后,會生成一個異常對象的拷貝,并不是直接傳遞給catch()里面的對象,而是將throw對象的拷貝傳給catch()中的對象,這個拷貝的臨時對象會被catch后銷毀。
#include <iostream>
#include <string>
using namespace std;
double Division(int x, int y)
{//除零錯誤,拋異常if (y == 0){string s("Division by zero condition!");throw s;}else{return ((double)x / (double)y);}
}void fun()
{int a;int b;cin >> a >> b;cout << Division(a, b) << endl;
}
int main()
{try{fun();}catch (const string ret){cout << ret << endl;}return 0;
}
我們拋出的是一個string的臨時對象s,出了作用域銷毀了,如果是直接傳給外邊,傳遞不過去。
如果我們拋異常了,打印的還是Division by zero condition!,就能夠說明生成了拷貝。
我們測試一次看看
我們發現確實存在拷貝,這和函數參數的傳遞有異曲同工之妙。
由于臨時對象具有常性,所以當拋出的對象是指針時一定注意在形參上加上const才能被接收。這也解釋了上面的代碼中為什么error_message的類型為什么是const char而不是char
🌟我們一般在catch最后邊加上一個catch(…),表示這個catch可以匹配任意類型,但是不知道異常錯誤是什么。
我們這個東西可以解決很大的問題,如果我們的異常沒有對應的catch,就會報錯,如果放到程序中,就會崩潰。
#include <iostream>
#include <string>
using namespace std;
double Division(int x, int y)
{//除零錯誤,拋異常if (y == 0){throw "Division by zero condition!";}else if(y==1){return ((double)x / (double)y);}else{throw 1;}
}void fun()
{int a;int b;cin >> a >> b;cout << Division(a, b) << endl;
}
int main()
{try{fun();}catch (const string ret){cout << ret << endl;}catch (...){cout << "Unkown error" << endl;}return 0;
}
我們如果輸入的第二個數為10,就會拋異常,類型為整形,但是外邊沒有對應的類型匹配,就會匹配到(…)中。
🌟有一種特殊情況,不用類型匹配:可以拋出子類對象,使用父類對象再catch中進行捕獲,這個在實際中是非常有用的。
在函數調用鏈中異常棧展開匹配原則
🌟1.查看throw是否在try中,如果在,就進行異常捕獲。
🌟2.如果存在匹配的catch,就調到對應的catch進行處理。
🌟3.如果這一層沒有匹配的catch,退出當前棧,就到上一層中進行尋找。
🌟4.如果再main函數中,也不存在匹配的catch就報錯,
沿著調用鏈查找匹配catch子句的過程稱為棧展開。
🌟5.異常捕獲與exit不同。exit直接退出程序,catch處理完異常之后,catch后面的代碼會正常執行。
#include <iostream>
#include <string>
using namespace std;
double Division(int x, int y)
{//除零錯誤,拋異常if (y == 0){throw "Division by zero condition!";}else if (y == 1){return ((double)x / (double)y);}
}void fun()
{int a;int b;cin >> a >> b;cout << Division(a, b) << endl;
}
int main()
{try{fun();}catch (const string ret){cout << ret << endl;}catch (...){cout << "Unkown error" << endl;}cout << "異常處理后繼續執行相關代碼" << endl;return 0;
}
2.異常的重新捕獲
有可能單個的catch不能完全處理一個異常,在進行一些校正處理以后,希望再交給更外層的調用
鏈函數來處理,catch則可以通過重新拋出將異常傳遞給更上層的函數進行處理。
我們捕獲這個異常并不是為了處理這個異常,而是為了干一些其他的事情之后,再把這個異常拋給上層,繼續處理。
我們舉一個例子理解一下
#include <iostream>
#include <string>
using namespace std;
double Division(int x, int y)
{//除零錯誤,拋異常if (y == 0){throw "Division by zero condition!";}else{return ((double)x / (double)y);}
}void fun()
{int*p = new int[10];int a;int b;cin >> a >> b;cout << Division(a, b) << endl;cout << "delete[] p" << endl;delete[] p;
}
int main()
{try{fun();}catch (const string ret){cout << ret << endl;}catch (...){cout << "Unkown error" << endl;}return 0;
}
在fun函數中new了一塊空間,現在拋異常看一下
我們new的空間并沒有被釋放,發生了內存泄漏
這時就需要用到異常的重新捕獲了,我們需要在fun函數中對這個異常進行一次捕獲,釋放new的空間,再拋給main函數。
為我們也可以直接在fun中捕獲異常,不在main中處理呀??
而在我們對異常進行一部分操作時,我們更愿意讓所有異常在main函數中進行統一處理,比如對異常進行記錄日志這樣的操作,此時需要重新使用throw拋出異常。
我們看一下解決代碼
#include <iostream>
#include <string>
using namespace std;
double Division(int x, int y)
{//除零錯誤,拋異常if (y == 0){throw "Division by zero condition!";}else{return ((double)x / (double)y);}
}void fun()
{int*p = new int[10];int a;int b;cin >> a >> b;try{cout << Division(a, b) << endl;}catch (...){cout << "delete[] p" << endl;delete[] p;throw;//捕到什么拋什么}cout << "delete[] p" << endl;delete[] p;
}
int main()
{try{fun();}catch (const string ret){cout << ret << endl;}catch (...){cout << "Unkown error" << endl;}return 0;
}
運行看一下
一旦catch捕獲異常,不能將異常用throw語句再次拋出,這句話是不對的。
三.異常安全與異常規范
1.異常安全
🌟構造函數時用來構造對象和初始化的,最好不要在構造函數拋異常,可能會導致對象不完整或者沒有完全初始化。
🌟析構函數是用來釋放空間的,對資源進行清理。最好不要在析構函數拋異常,可能會導致資源泄露(內存泄漏,句柄未關閉)。
🌟C++中異常經常會導致資源泄露問題,比如在new和delete中拋異常,導致內存泄漏,在lock和unlock之間拋出異常1導致死鎖等問題,我們將會用智能指針來解決這個問題。
2.異常規范
我們在書寫異常時,可以拋出任意類型的對象, 異常規格說明的目的是為了讓函數使用者知道該函數可能拋出的異常有哪些。
C++98,建議,并不會強制報錯,如果我們不這樣做了只會有警告,這個是為了兼容c語言做出的讓步。
🌟我們可以在函數頭后買你加上throw( ),括號里存放可能拋出的異常類型。
throw(char,int char*)就表明了這個函數可能拋出char,int,char這三種類型的異常。
🌟這里表示這個函數只會拋出bad_alloc的異常
void operator new (std::size_t size) throw (std::bad_alloc);
🌟throw(),表明這個函數只會不會拋出異常
🌟如果無異常接口聲明,此函數可以拋任意類型的異常
但是有些異常類型是非常復雜的,為了寫出可能發生的異常類型,代價會很大,而且寫時太繁瑣寫出來也不美觀。因此,這個建議性的規范很少有人用,也正因為它只是一個建議,所以不使用或者不按要求使用也不會報錯。
#include <iostream>
#include <string>
using namespace std;
double Division(int x, int y) throw()//表明不會拋異常
{//除零錯誤,拋異常if (y == 0){throw "Division by zero condition!";}else{return ((double)x / (double)y);}
}void fun()
{int a;int b;cin >> a >> b;cout << Division(a,b) << endl;
}
int main()
{try{fun();}catch (const string ret){cout << ret << endl;}catch (...){cout << "Unkown error" << endl;}return 0;
}
但是如果我們硬要拋異常呢??
只會存在警告,不會強制報錯。
C++11
🌟noexcept,表明函數不會拋異常
但如果我們還是硬拋異常會怎樣呢???
代碼就給我們直接掛掉了。
四.自定義異常體系
在以后寫代碼的時候會遇到小組合作的形式,每個小組負責不同的模塊,每個小組都會拋出異常,但是每個小組拋出的異常類型不同,放在一起在main函數中進行捕捉就會很復雜。
實際中拋出和捕獲的匹配原則有一個例外,類型可以不完全匹配,拋出子類對象用父類進行捕捉。每個小組都可以拋出派生類的異常,在mian函數中使用基類統一捕捉。
實際項目中,可以創建一個父類,父類中有一個錯誤碼(int)和錯誤描述信息(string),在這個類里面創建一個虛函數用于返回內部的string對象方便使用人員打印。
class Exception
{
public:Exception(const string& errmsg, int id):_errmsg(errmsg), _id(id){}virtual string what() const{return _errmsg;}
protected:string _errmsg;int _id;
};
不同的小組拋出的異常都會具有本小組的特點,用繼承的方式創建一個類,子類中添加一個成員變量記錄當前模塊的特殊錯誤信息,并且該類所對應的what函數就可以通過重寫來添加一個標志性內容,我們就可以更加容易的知道哪里出了問題。
class SqlException : public Exception
{
public:SqlException(const string& errmsg, int id, const string& sql):Exception(errmsg, id), _sql(sql){}virtual string what() const{string str = "SqlException:";str += _errmsg;str += "->";str += _sql;return str;}
private:const string _sql;
};
class CacheException : public Exception
{
public:CacheException(const string& errmsg, int id):Exception(errmsg, id){}virtual string what() const{string str = "CacheException:";str += _errmsg;return str;}
};
class HttpServerException : public Exception
{
public:HttpServerException(const string& errmsg, int id, const string& type):Exception(errmsg, id), _type(type){}virtual string what() const{string str = "HttpServerException:";str += _type;str += ":";str += _errmsg;return str;}
private:const string _type;
};
那么有了這些描述異常的父子類之后就可以用下面的代碼來測試異常:
void SQLMgr()
{srand(time(0));if (rand() % 7 == 0){throw SqlException("權限不足", 100, "select * from name = '張三'");}//throw "xxxxxx";
}
void CacheMgr()
{srand(time(0));if (rand() % 5 == 0){throw CacheException("權限不足", 100);}else if (rand() % 6 == 0){throw CacheException("數據不存在", 101);}SQLMgr();
}
void HttpServer()
{// ...srand(time(0));if (rand() % 3 == 0){throw HttpServerException("請求資源不存在", 100, "get");}else if (rand() % 4 == 0){throw HttpServerException("權限不足", 101, "post");}CacheMgr();
}
int main()
{while (1){this_thread::sleep_for(chrono::seconds(1));try {HttpServer();}catch (const Exception& e) // 這里捕獲父類對象就可以{// 多態cout << e.what() << endl;}catch (...){cout << "Unkown Exception" << endl;}}return 0;
}
main函數中首先調用httpserver函數,生成一個隨機數,我們把這個數字看作一種情況,如果這個數字能被3 和4整除那么這種情況就出錯了,直接使用throw來拋出異常,如果沒有出錯的花就調用緩沖區的函數,這個函數里面也是相同道理,在這個函數之后就調用數據庫的相關函數遇到一些情況就拋出異常,那么在main函數里面我們就可以統一使用父類類型的catch來統一捕獲異常,并使用里面的what函數來打印出從的內容,那么上面的代碼運行的結果如下:
五.C++標準庫的異常體系
C++ 提供了一系列標準的異常,定義在 中,我們可以在程序中使用這些標準的異常。它們是以父
子類層次結構組織起來的,如下所示:
由于C++提供的異常體系對項目開發中的異常幫助十分有限,所以這個標準幾乎沒人用。’
六.異常優缺點
優點
🌟1.相比于錯誤碼,異常可以清晰準確的展現出錯誤信息,甚至包含堆棧調用的信息,幫助我們更好的定位bug
🌟2.異常會進行多層跳轉,不用層層返回進行判斷,直到最外層拿到錯誤。
int ConnnectSql()
{// 用戶名密碼錯誤if (...)return 1;// 權限不足if (...)return 2;
}int ServerStart() {if (int ret = ConnnectSql() < 0)return ret;int fd = socket()if(fd < 0)return errno;
}int main()
{if (ServerStart() < 0)...return 0;
}
下面這段偽代碼我們可以看到ConnnectSql中出錯了,先返回給ServerStart,ServerStart再返回給main函數,main函數再針對問題處理具體的錯誤。
如果是異常體系,不管是ConnnectSql還是ServerStart及調用函數出錯,都不用檢查,因為拋出的異常異常會直接跳到main函數中catch捕獲的地方,main函數直接處理錯誤。
🌟2.很多的第三方庫都包含異常,比如boost、gtest、gmock等等常用的庫,那么我們使用它們也需要使用異常
🌟4.部分函數使用異常更好處理。
T& operator這樣的函數,如果pos越界了只能使用異常或者終止程序處理,沒辦法通過返回值表示錯誤
缺點:
🌟1.導致執行流亂跳,導致我們追蹤調式以及分析程序時,比較困難。
🌟2.異常會有一些性能的開銷,。當然在現代硬件速度很快的情況下,這個影響基本忽略不計。
🌟3.C++沒有垃圾回收機制,資源需要自己管理,有可能發生內存泄漏,死鎖等安全問題。
🌟4.C++標準庫的異常體系定義得不好,導致大家各自定義各自的異常體系,非常的混亂。
🌟異常盡量規范使用,否則后果不堪設想,隨意拋異常,外層捕獲的用戶苦不堪言。
練習題
如何捕獲異常可以使得代碼通過編譯? ()
class A
{
public:
??A(){}
};
void foo()
{? throw new A; }
A.catch (A x)
B.catch (A * x)
C.catch (A & x)
D.以上都不是
正確答案是B, 異常是按照類型來捕獲的,throw后拋出的是A*類型的異常,因此要按照指針方式進行捕獲
總結
以上就是今天要講的內容,本文僅僅詳細介紹了C++異常的內容。希望對大家的學習有所幫助,僅供參考 如有錯誤請大佬指點我會盡快去改正 歡迎大家來評論~~ 😘 😘 😘