在沒學習c++之前,我們如果在程序中遇到異常錯誤,可能會用以下的處理方式
- 1.終止程序,如assert,缺陷:用戶難以接受,如果發生內存錯誤,除0錯誤就會終止程序。
- 2.返回錯誤碼,缺陷:需要程序員自己去查找對應的錯誤。如系統的很多庫的接口函數都是把錯誤碼放到errno中,表示錯誤。
一.c++異常
異常是一種處理錯誤的方式。當一個函數發現自己無法處理的錯誤的時候,就會拋出異常,讓函數直接或者間接的調用着來處理這個錯誤。
- throw:當問題出現時,程序會拋出一個異常。這是通過使用throw關鍵字來完成的。
- catch:在想要處理問題的地方,通過異常處理程序捕獲異常。catch關鍵字用於捕獲異常,可以有多個catch進行捕獲。
- try:try塊中的代碼標識將被激活的特定異常,它後面通常跟着一個或者多個catch塊。
如果有一個塊拋出了異常,捕獲異常的方法會使用try和catch關鍵字。try塊中放置了可能會拋出異常的代碼,try塊中的代碼被稱爲保護代碼。
二.異常的使用
1.異常的拋出和匹配原則
-
(1)異常是通過拋出對象而引發的,該對象的類型決定了應該激活哪個catch的處理代碼。
-
(2)被選中的處理代碼是調用鏈中與該對象類型匹配且離拋出異常位置最近的那一個。
-
(3)拋出異常對象後,會生成一個異常對象的拷貝,因爲拋出的異常對象可能是一個臨時對象,所以會生成一個拷貝對象,這個拷貝的臨時對象會在被catch之後銷燬
-
(4)catch(。。。)可以捕獲任何類型的異常,問題是不知道異常錯誤是什麼
-
(5)實際中拋出和捕獲的匹配原則有個例外,並不都是類型匹配原則,可以拋出派生類對象,使用基類對象進行捕獲。
2.在函數調用鏈中異常棧展開匹配原則
-
(1)首先檢查throw是否在try的內部,如果是再查找匹配的catch語句。如果有匹配的,則調到catch的地方進行處理。
-
(2)沒有匹配的catch則退出當前函數棧,繼續在調用函數的棧中進行查找匹配的catch。
-
(3)如果到達main函數的棧中,依舊沒有匹配的,則終止程序,上述這個沿着調用鏈查找匹配的catch子句的過程稱之爲棧展開。所以實際上我們最後都要加上一個catch(。。。)捕獲任意類型的異常,否則當有異常沒捕獲,程序就會自動終止。
-
(4)找到匹配的catch子句並處理以後,會繼續沿着catch子句後面進行執行。
3.異常的重新拋出
有可能單個的catch不能完全處理一個異常,在進行一些校正處理之後,希望再交給更外層的調用鏈函數來處理,catch則可以通過重新拋出將異常傳遞給更上層的函數來進行處理。
#include<iostream>
using namespace std;
double Division(int a, int b)
{
if (b == 0)
{
throw"Division by zero condition!";
}
return (double)a / (double)b;
}
void Func()
{
//這裏可以看到如果發生除0錯誤拋出異常,另外下面的array沒有得到釋放
//所以這裏捕獲異常後並不處理異常,異常還是交給外面處理
//這裏捕獲了異常再重新拋出去。
int * array = new int[10];
try{
int len, time;
cin >> len >> time;
cout << Division(len, time) << endl;
}
catch (...)
{
cout << "delete []" << array << endl;
delete[] array;
throw;
}
cout << "delete []" << array << endl;
delete[] array;
}
int main()
{
try
{
Func();
}
catch (const char* errmsg)
{
cout << errmsg << endl;
}
return 0;
}
4.異常的安全
-
(1)構造函數完成對象的初始化,最好不要再構造函數中拋出對象,否則可能會導致對象不完整或沒有完全初始化。
-
(2)析構函數主要完成資源的清理,最好不要在析構函數內部拋出異常,否則可能會導致資源泄漏。
-
(3)c++異常經常會導致資源泄漏的問題,比如在new和delete中拋出了異常,導致內存泄漏,在lock和unlock之間拋出了異常導致死鎖,c++經常使用RAII來解決以上問題。
5.異常規範
- (1)異常規格說明的目的是爲了讓函數使用者知道該函數可能拋出的異常有哪些。 可以在函數的後面接 throw(類型),列出這個函數可能拋擲的所有異常類型。
- (2)函數的後面接throw(),表示函數不拋異常。
- (3)若無異常接口聲明,則此函數可以拋擲任何類型的異常。
// 這裏表示這個函數會拋出A/B/C/D中的某種類型的異常
void fun() throw(A,B,C,D);
// 這裏表示這個函數只會拋出bad_alloc的異常
void* operator new (std::size_t size) throw (std::bad_alloc);
// 這裏表示這個函數不會拋出異常
void* operator new (std::size_t size, void* ptr) throw();
三.自定義異常體系
實際上很多的公司都會自定義自己的異常體系進行規範的異常處理,因爲一個項目中如果大家隨意拋出異常,那麼外層的調用者基本上就沒辦法玩了,所以實際中都會定義一套繼承的規範體系,這樣大家拋出的都是繼承的派生類對象,捕獲一個基類就可以了。
// 服務器開發中通常使用的異常繼承體系
class Exception
{
protected:
string _errmsg;
int _id;
//list<StackInfo> _traceStack;
// ...
};
class SqlException : public Exception
{};
class CacheException : public Exception
{};
class HttpServerException : public Exception
{};
int main()
{
try{
// server.Start();
// 拋出對象都是派生類對象
}
catch (const Exception& e)
// 這裏捕獲父類對象就可以
{}
catch (...)
{
cout << "Unkown Exception" << endl;
}
return 0;
}
四.異常的優缺點
1.c++異常的優點:
- (1)異常對象定義好了,相比錯誤碼的方式可以清晰準確的展示出錯誤的各種信息,甚至可以包含堆棧調用的信息,這樣可以幫助更好的定位程序中的bug。
- (2)返回錯誤碼的傳統方式中有個很大的問題就是,在函數調用堆棧中,深層的函數返回了錯誤,那麼,我們就得層層返回錯誤,最外層才能拿到錯誤。
//1.下面這段僞代碼我們可以看到ConnnectSql中出錯了,先返回給ServerStart,
//ServerStart再返回 給main函數,main函數再針對問題處理具體的錯誤。
// 2.如果是異常體系,不管是ConnnectSql還是ServerStart及調用函數出錯,
//都不用檢查,因爲拋出的 異常異常會直接跳到main函數中catch捕獲的地方,main函數直接處理錯誤。 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;
}
- (3)很多的第三方庫都包含異常,比如boost、gtest、gmock等等常用的庫,那麼我們使用它們也需要使用 異常。
- (4)很多測試框架都使用異常,這樣能更好的使用單元測試等進行白盒的測試。
- (5)部分函數使用異常更好處理,比如構造函數沒有返回值,不方便使用錯誤碼方式處理。比如T& operator這樣的函數,如果pos越界了只能使用異常或者終止程序處理,沒辦法通過返回值表示錯誤。
2.c++異常的缺點
- (1)異常會導致程序的執行流亂跳,並且非常的混亂,並且是運行時出錯拋異常就會亂跳。這會導致我們跟 蹤調試時以及分析程序時,比較困難。
- (2)異常會有一些性能的開銷。當然在現代硬件速度很快的情況下,這個影響基本忽略不計。
- (3)C++沒有垃圾回收機制,資源需要自己管理。有了異常非常容易導致內存泄漏、死鎖等異常安全問題。 這個需要使用RAII來處理資源的管理問題。學習成本較高。
- (4)C++標準庫的異常體系定義得不好,導致大家各自定義各自的異常體系,非常的混亂。
- (5)異常儘量規範使用,否則後果不堪設想,隨意拋異常,外層捕獲的用戶苦不堪言。所以異常規範有兩 點:一、拋出異常類型都繼承自一個基類。二、函數是否拋異常、拋什麼異常,都使用 func() throw();的方式規範化。