C語言傳統的處理錯誤的方式
傳統的錯誤處理機制:
1. 終止程序, 如assert.
缺陷: 用戶難以接受, 如發生內存錯誤, 除0錯誤時就會終止程序.
如果assert括號里面的表達式結果為假, 那么assert就會中斷程序并報錯, 所以使用assert可以幫助我們在程序判斷一些可能出錯的地方.
#include <assert.h>
int main()
{int x, y;cin >> x >> y;assert(y != 0);cout << x / y << endl;return 0;
}
2. 返回錯誤碼,?缺陷: 需要程序員自己去查找對應的錯誤, 如系統的很多庫的接口函數都是通
過把錯誤碼放到errno中, 表示錯誤.
實際中C語言基本都是使用返回錯誤碼的方式處理錯誤, 部分情況下使用終止程序處理非常嚴重的錯誤。
c++異常的使用
異常是一種處理錯誤的方式,?當一個函數發現自己無法處理的錯誤時就可以拋出異常, 讓函數的
直接或間接的調用者處理這個錯誤。
c++處理錯誤需要三個關鍵字 try, throw, catch.
???????throw: 當問題出現時, 程序會拋出一個異常, 這個異常對象可以是任意的類型
catch: 在想要處理問題的地方, 通過異常處理程序捕獲異常. catch 關鍵字用于捕獲異
常, 可以有多個catch進行捕獲.?
try: try 塊中的代碼標識將被激活的特定異常,它后面通常跟著一個或多個 catch 塊.
如果有一個塊拋出一個異常, 捕獲異常的方法會使用 try 和 catch 關鍵字. try 塊中放置可能拋
出異常的代碼, try 塊中的代碼被稱為保護代碼. 使用 try/catch 語句的語法如下所示:
try
{// 保護的標識代碼
}catch( ExceptionName e1 )
{// catch 塊
}catch( ExceptionName e2 )
{// catch 塊
}catch( ExceptionName eN )
{// catch 塊
}
首先我們有個名為func函數, 在函數里面我們要接收兩個參數, 并打印這兩個參數相除的結果, 那么這個時候我們就再創建一個函數來實現兩個參數相除, 并將相除的結果進行返回:?
double Division(int a, int b)
{// 當b == 0時拋出異常if (b == 0)throw "Division by zero condition!";elsereturn ((double)a / (double)b);
}void Func()
{int len, time;cin >> len >> time;cout << Division(len, time) << endl;
}int main()
{try {Func();} catch (const char* errmsg){cout << errmsg << endl;}return 0;
}
1. 當我們發現異常之后就得使用throw來拋出異常, 那這里拋出的異常對象我們就可以使用字符串, 字符串的內容為:?
Division by zero condition!
2. 函數寫完之后就可以在main函數里面進行Func函數來運行我們寫的函數, 又因為這個函數里面可能會拋出異常, 所以我們把這個Func函數放到try塊里面.
3. 在try塊里面拋出了異常就得在try塊的外面使用catch來進行接收, 因為拋出的是字符串類型的異常對象, 所以在使用catch接收的時候就得在catch后面的括號中寫入字符串類型的參數來進行接收,在catch對應的語句塊里就可以把對應的錯誤提示輸出到屏幕上面.
異常的規則?
異常的拋出和匹配原則
1. 異常是通過拋出對象而引發的, 該對象的類型決定了應該激活哪個catch的處理代碼.
double Division(int a, int b)
{// 當b == 0時拋出異常if (b == 0)throw "Division by zero condition!";elsereturn ((double)a / (double)b);}void Func()
{int len, time;cin >> len >> time;cout << Division(len, time) << endl;
}int main()
{try{Func();}catch (const char c){cout << c << endl;}/*catch (const char* errmsg){cout << errmsg << endl;}*/return 0;
}
?假如將catch的參數換成char類型, 和拋出對象不匹配.
不會調用catch, 會像之前一樣直接終止程序.
2. 被選中的處理代碼是調用鏈中與該對象類型匹配且離拋出異常位置最近的那一個. 在同一個作用域里面不能有相同有相同類型的catch語句, 但是在不同的作用域里面可以存在相同的catch語句.
double Division(int a, int b)
{// 當b == 0時拋出異常if (b == 0)throw "Division by zero condition!";elsereturn ((double)a / (double)b);}void Func()
{int len, time;cin >> len >> time;try{cout << Division(len, time) << endl;}catch (const char* errmsg){cout <<"我是離throw最近的catch: " << errmsg << endl;}
}int main()
{try{Func();}catch (const char* errmsg){cout << errmsg << endl;}return 0;
}
?
這個"近"的概念和catch的匹配原則有關, 如果執行了throw, 會先在本函數(Division)里檢查throw是否在try里, 如果有就會去匹配對應的catch語句, 有try一定有catch,不然會報錯.
如果不在try塊里或者沒找到匹配的catch, 就會退出當前函數棧幀(Division), 繼續到調用這個函數的函數棧幀(func)中去尋找catch, 找不到就繼續遞歸尋找, 如果main函數里也沒有匹配的catch直接終止程序. 上面的例子就是在throw不在try塊里, 所以去調用Division的函數里找catch并找到了匹配的catch.
雖然catch語句可以存在多個, 但是拋出的異常在被捕捉的時候只能被一個catch語句所捕捉, 一旦一個catch語句捕捉成功了, 那么其他的catch語句都不會被執行, 所以異常是通過拋出對象而引發的, 該對象的類型決定了應該激活哪個catch的處理代碼.
當catch語句執行完之后還可以繼續執行后面的內容:??
double Division(int a, int b)
{try {// 當b == 0時拋出異常if (b == 0)throw "Division by zero condition!";elsereturn ((double)a / (double)b);}catch (const char* errmsg){cout<< "我是離throw最近的catch: " << errmsg << endl;}cout << "捕捉異常后, 我會繼續執行" << endl;
}void Func()
{int len, time;cin >> len >> time;try{cout << Division(len, time) << endl;}catch (const char* errmsg){cout << errmsg << endl;}
}int main()
{try{Func();}catch (const char* errmsg){cout << errmsg << endl;}return 0;
}
throw拋出異常后如果沒有找到catch后面的語句會正常執行嗎?
double Division(int a, int b)
{// 當b == 0時拋出異常if (b == 0)throw "Division by zero condition!";elsereturn ((double)a / (double)b);cout << "異常拋出后我會執行嗎" << endl;
}void Func()
{int len, time;cin >> len >> time;try{cout << Division(len, time) << endl;}catch (const char c){}cout << "異常拋出后我會執行嗎" << endl;
}int main()
{try{Func();}catch (const char* errmsg){cout << errmsg << endl;}return 0;
}
可以看到division和func里都沒有catch或者匹配的catch, 后面的語句都沒有執行.
注意, 上面說的catch語句執行完之后還可以繼續執行后面的內容是指已經catch結束了會執行后面的語句, 這里還是處于異常拋出階段, 是在匹配catch.
那我在func里事先定義一個對象, 沒有匹配的catch的話會調用析構嗎??
class A
{
public:A(){cout << "A()" << endl;}~A(){cout << "~A()" << endl;}
};double Division(int a, int b)
{// 當b == 0時拋出異常if (b == 0)throw "Division by zero condition!";elsereturn ((double)a / (double)b);cout << "捕捉異常后, 我會繼續執行" << endl;
}void Func()
{int len, time;cin >> len >> time;A a;try{cout << Division(len, time) << endl;}catch (const char c){}cout << "異常拋出后我會執行嗎" << endl;
}int main()
{try{Func();}catch (const char* errmsg){cout << errmsg << endl;}return 0;
}
?析構還是會調用的, 因為它雖然是一個棧幀一個棧幀去找catch, 但是沒找到的話棧幀還是會銷毀的.?
在函數調用鏈中異常棧展開匹配原則
1. 首先檢查throw本身是否在try塊內部, 如果是再查找匹配的catch語句. 如果有匹配的, 則
調到catch的地方進行處理.?
2. 沒有匹配的catch則退出當前函數棧, 繼續在調用函數的棧中進行查找匹配的catch.
3. 如果到達main函數的棧, 依舊沒有匹配的, 則終止程序, 上述這個沿著調用鏈查找匹配的
catch子句的過程稱為棧展開. 所以實際中我們最后都要加一個catch(...)捕獲任意類型的異
常, 否則當有異常沒捕獲, 程序就會直接終止.(下面會說)
4. 找到匹配的catch子句并處理以后,會繼續沿著catch子句后面繼續執行。?
3. 拋出異常對象后, 會生成一個異常對象的拷貝, 因為拋出的異常對象可能是一個臨時對象, 所以會生成一個拷貝對象, 這個拷貝的臨時對象會在被catch以后銷毀.(這里的處理類似于函數的傳值返回)
double Division(int x, int y)
{if (y == 0){string s("除0錯誤");throw s;}else{return (double)x / (double)y;}
}void Func()
{A a;try {int x, y;cin >> x >> y;cout << Division(x, y) << endl;}catch (const char s){cout << s << endl;}
}int main()
{try{Func();}catch (const string& s){cout << s << endl;}return 0;
}
這里拋出異常選擇拋出一個string類型的對象, 但是這里異常拋出的是創建出來的s嗎? 肯定不是, 因為s是一個局部對象, s的生命周期在當前作用域, 出了作用域就會銷毀, 所以會產生一個臨時對象的拷貝, 拷貝什么時候銷毀? catch用完了就會銷毀. 那拷貝的代價會不會很大? 拷貝的代價肯定不小, 但是有了移動構造代價就小了很多, 因為深拷貝的對象如果是右值會調用移動構造, 淺拷貝的對象就要按字節進行拷貝了, 但是淺拷貝的對象一般也不會很大.
4. 寫代碼的時候可能會遇到各種各樣的異常, 如果有一個新的異常沒有被以往的catch捕獲到, 那么最終到main函數還沒捕捉到, 就會程序崩潰, 因為一個小的異常而導致整個項目崩潰, 代價太大.
double Division(int x, int y)
{if (y== 0){string s("除0錯誤");throw s;}else{return (double)x / (double)y;}
}void f1()
{throw 1;
}void Func()
{A a;try {int x, y;cin >> x >> y;cout << Division(x, y) << endl;}catch (const char s){cout << s << endl;}f1();
}int main()
{while (1){try{Func();}catch (const string& s){cout << s << endl;}}return 0;
}
假如我當前的程序是一個循環狀態,??現在在func里寫了一個函數只要運行就拋異常, 而且這個異常還沒辦法被catch, 就會導致程序崩潰.
?那么為了能捕獲所有的異常,?c++提供了一個這樣類型的捕捉:?catch(...), 它可以捕獲任意類型的異常.
int main()
{while (1){try{Func();}catch (const string& s){cout << s << endl;}catch (...)//走到這里說明有人沒有按 規范/約定 來拋異常{cout << "未知錯誤" << endl;}}return 0;
}
5. 實際中拋出和捕獲的匹配原則有個例外, 并不都是類型完全匹配, 可以拋出的派生類對象, 使用基類捕獲, 這個在實際中非常實用.
自定義異常體系?
實際中都會自定義自己的異常體系進行規范的異常管理, 因為一個項目中如果大家隨意拋異常, 那么外層的調用者就沒辦法處理了, 所以實際中都會定義一套繼承的規范體系.
這樣大家拋出的都是繼承的派生類對象, 捕獲一個基類就可以了?
如果每個小組分別負責數據庫模塊, 緩存模塊, 業務模塊, 每個小組都會拋出異常, 但是每個小組拋出的異常類型又不同, 如果一起在main函數里面進行捕捉的話就會非常的多并且非常的復雜, 并且每個小組所拋出的異常都有著自己的模塊的屬性, 所以使用一個類來解決這里的異常就不現實.
所以定義一個基類的異常, 每個小組拋出這個基類的派生類異常, 在main函數里面使用基類來統一捕獲. 比如這里的父類里面有個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; // 錯誤編號
};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 = '張三'");}}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){Sleep(500);try {HttpServer();}catch (const Exception& e) // 這里捕獲父類對象就可以{// 多態cout << e.what() << endl;}catch (...){cout << "Unkown Exception" << endl;}}return 0;
}
異常的重新拋出
有可能單個的catch不能完全處理一個異常, 在進行一些校正處理以后, 希望再交給更外層的調用鏈函數來處理, catch則可以通過重新拋出將異常傳遞給更上層的函數進行處理.
首先看一個場景:??
double Division(int a, int b)
{// 當b == 0時拋出異常if (b == 0){throw "Division by zero condition!";}return (double)a / (double)b;
}void Func()
{// 這里可以看到如果發生除0錯誤拋出異常,另外下面的array沒有得到釋放。int* array1 = new int[10];// 異常安全問題int len, time;cin >> len >> time;cout << Division(len, time) << endl;cout << "delete []" << array1 << endl;delete[] array1;}int main()
{try{Func();}catch (const char* errmsg){cout << errmsg << endl;}return 0;
}
Func函數里面沒有捕獲異常, 但是在Func函數里面我們使用new申請了一段空間, 并且在調用Division函數之后使用delete將這段空間給釋放掉了, 這么寫如果沒有拋出異常的話是沒有問題的:?
但是一旦這里拋出了異常那么就會發生內存泄漏:
原因就是Division函數里面發生了異常, throw拋出一個異常之后Func函數里面沒有catch語句來進行接收, 所以就會來到main函數里面被main函數里面的catch所接收, 也就意味著不會執行func函數剩下的代碼, 所以就發生了泄漏, 在之前的學習中我們認為內存泄漏多半是因為編寫代碼的人忘記了使用delete來釋放空間而導致的內存泄漏, 而現在即使寫了delete也有可能因為異常的執行流導致內存泄漏.
解決方法:
所有寫new和delete的地方都要加一個try catch, 并重新拋出異常:
那樣寫比較麻煩, 因為如果要拋出多個異常還要為每個異常重新拋出一次, 所以直接catch(...):
double Division(int a, int b)
{// 當b == 0時拋出異常if (b == 0){throw "Division by zero condition!";}return (double)a / (double)b;
}void Func()
{// 這里可以看到如果發生除0錯誤拋出異常,另外下面的array沒有得到釋放。// 所以這里捕獲異常后并不處理異常,異常還是交給外面處理,這里捕獲了再// 重新拋出去。int* array1 = new int[10];// 異常安全問題try{int len, time;cin >> len >> time;cout << Division(len, time) << endl;}catch(...){cout << "delete []" << array1 << endl;delete[] array1;throw;//捕捉什么拋出什么}cout << "delete []" << array1 << endl;delete[] array1;
}int main()
{try{Func();}catch (const char* errmsg){cout << errmsg << endl;}return 0;
}
現在有一個問題, 如果我new了兩個對象呢??
double Division(int a, int b)
{// 當b == 0時拋出異常if (b == 0){throw "Division by zero condition!";}return (double)a / (double)b;
}void Func()
{// 這里可以看到如果發生除0錯誤拋出異常,另外下面的array沒有得到釋放。// 所以這里捕獲異常后并不處理異常,異常還是交給外面處理,這里捕獲了再// 重新拋出去。int* array1 = new int[10];int* array2 = new int[10];// 異常安全問題try{int len, time;cin >> len >> time;cout << Division(len, time) << endl;}catch(...){cout << "delete []" << array1 << endl;delete[] array1;cout << "delete []" << array2 << endl;delete[] array2;throw;//捕捉什么拋出什么}cout << "delete []" << array1 << endl;delete[] array1;cout << "delete []" << array2 << endl;delete[] array2;
}int main()
{try{Func();}catch (const char* errmsg){cout << errmsg << endl;}catch (const exception& e){cout << e.what();}return 0;
}
也可以像上面那樣去處理, 但是如果我在new的地方拋了異常呢??如果在new array1的時候拋出了異常, 那么就會匹配catch(const exception& e), 但是如果new array2的時候拋出了異常, array1就無法正常delete了, 解決方法會在智能指針章節介紹.
異常安全?
1. 構造函數完成對象的構造和初始化, 最好不要在構造函數中拋出異常, 否則可能導致對象不完整或沒有完全初始化.
2. 析構函數主要完成資源的清理, 最好不要在析構函數內拋出異常, 否則可能導致資源泄漏(內存泄漏, 句柄未關閉等)
3.C++中異常經常會導致資源泄漏的問題, 比如在new和delete中拋出了異常, 導致內存泄漏, 在lock和unlock之間拋出了異常導致死鎖, C++經常使用RAII來解決以上問題, RAII在智能指針介紹.
異常規范?
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 delete (std::size_t size, void* ptr) throw();
// C++11 中新增的noexcept,表示不會拋異常
thread() noexcept;
thread (thread&& x) noexcept;
雖然C++98的throw()和C++11的noexcept都代表不會拋異常, 但它們有一點區別, 如果throw()表示的函數內拋出了異常編譯運行都可以通過, 如果noexcept表示的韓式拋出了異常編譯時可以通過, 但是運行就會報錯.?
C++標準庫的異常體系
C++ 提供了一系列標準的異常, 定義在 中. 我們可以在程序中使用這些標準的異常, 它們是以父子類層次結構組織起來的, 如下所示:?
?
說明: 實際中我們可以可以去繼承exception類實現自己的異常類. 但是實際中往往像上面一樣自己定義一套異常繼承體系. 因為C++標準庫設計的不夠好用.
?異常的優缺點
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. 部分函數使用異常更好處理, 比如構造函數沒有返回值, 不方便使用錯誤碼方式處理. 比如T& operator這樣的函數, 如果pos越界了只能使用異常或者終止程序處理, 沒辦法通過返回值表示錯誤.
C++異常的缺點:
1. 異常會導致程序的執行流亂跳, 并且非常的混亂, 并且是運行時出錯拋異常就會亂跳. 這會
導致我們跟蹤調試時以及分析程序時, 比較困難.
2. 異常會有一些性能的開銷. 當然在現代硬件速度很快的情況下, 這個影響基本忽略不計.
3. C++沒有垃圾回收機制,?資源需要自己管理. 有了異常非常容易導致內存泄漏、死鎖等異常安全問題. 這個需要使用RAII來處理資源的管理問題.
4. C++標準庫的異常體系定義得不好, 導致大家各自定義各自的異常體系, 非常的混亂.
5. 異常盡量規范使用, 否則后果不堪設想, 隨意拋異常, 外層捕獲的用戶苦不堪言, 所以異常規范有兩點:一、拋出異常類型都繼承自一個基類.
二、函數是否拋異常, 拋什么異常, 都使用 func()?throw(異常類型)/noexception; 的方式規范化.
總結: 異常總體而言, 利大于弊, 所以工程中我們還是鼓勵使用異常的, 另外OO的語言基本都是
用異常處理錯誤, 這也可以看出這是大勢所趨.