C++?異常處理機制與自定義異常體系處理方式
前言??????
在程序開發(fā)中,錯誤和異常的處理是至關(guān)重要的,它直接影響到程序的健壯性和穩(wěn)定性。C語言的錯誤處理主要依賴返回值和錯誤碼,雖然這種方式簡單直接,但在復(fù)雜的程序中,錯誤處理代碼往往難以維護且容易出錯。相比之下,C++引入的異常處理機制提供了一種更為高效和靈活的錯誤處理方式,使得程序的錯誤管理更加清晰和優(yōu)雅。
本節(jié)將詳細介紹C++異常處理的相關(guān)概念、用法以及如何通過自定義異常體系來滿足程序的需求。同時,我們將對比C語言的傳統(tǒng)錯誤處理方式,分析C++異常機制的優(yōu)缺點,并探討標(biāo)準(zhǔn)庫中提供的異常體系,幫助開發(fā)者更好地理解和使用C++的異常處理功能。
1.C語言傳統(tǒng)的處理錯誤的方式 ??
C語言傳統(tǒng)的錯誤處理機制主要有兩種方式:終止程序和返回錯誤碼。這兩種方式雖然簡單易用,但各自也有其局限性和缺陷,尤其是在處理復(fù)雜錯誤或大規(guī)模程序時,往往會導(dǎo)致維護上的困難。
1. 終止程序
一種常見的錯誤處理方式是直接終止程序,這種方式通常是通過使用assert
宏實現(xiàn)的。assert
會在程序運行時對條件進行檢查,如果條件不滿足,則程序會立即終止并輸出錯誤信息。
代碼如下:
#include <assert.h> void foo(int x) { assert(x != 0); // 如果x為0,程序終止 printf("x is not zero\n"); } int main() { foo(0); // 觸發(fā)斷言,程序終止 return 0; }
缺陷:
- 用戶難以接受:程序直接終止,尤其是當(dāng)發(fā)生一些小錯誤或邊界條件時,用戶體驗很差。
- 適用場景有限:這種方式適用于嚴重錯誤(如內(nèi)存錯誤、除0錯誤等),但不適合所有情況,因為大多數(shù)程序錯誤并不需要完全終止程序。
當(dāng)程序遇到無法恢復(fù)的錯誤時,assert
可以有效地幫助開發(fā)者檢測出問題。但是,當(dāng)程序出現(xiàn)一些非致命錯誤時,用戶希望程序能夠優(yōu)雅地處理,而不是直接崩潰。
2. 返回錯誤碼
另一個常見的錯誤處理方式是通過返回錯誤碼來通知程序出現(xiàn)了問題。這種方法在C語言中非常普遍,許多標(biāo)準(zhǔn)庫函數(shù)(如malloc
、fopen
等)都通過返回一個特殊的錯誤碼來表示函數(shù)執(zhí)行失敗。開發(fā)者需要根據(jù)返回值來判斷錯誤,并做相應(yīng)的處理。
比如,malloc
在分配內(nèi)存失敗時返回NULL
,fopen
在打開文件失敗時返回NULL
,errno
則是一個全局變量,用于記錄最近一次系統(tǒng)調(diào)用的錯誤碼。
缺陷:
- 需要手動檢查錯誤碼:程序員必須檢查每個函數(shù)調(diào)用的返回值,以便發(fā)現(xiàn)錯誤。這導(dǎo)致了大量重復(fù)的錯誤處理代碼,增加了維護成本。
- 錯誤信息不夠直觀:雖然可以通過
errno
來獲取詳細的錯誤信息,但這通常不如異常機制直觀。錯誤碼本身通常是數(shù)字,缺乏對錯誤本質(zhì)的描述,需要額外的邏輯去理解錯誤碼的含義。 - 錯誤處理分散:程序中多處調(diào)用的函數(shù)可能會返回不同的錯誤碼,處理這些錯誤的邏輯往往分散在代碼的各個地方,導(dǎo)致代碼的可讀性差。
3.實際使用中的情況
在實際的C語言開發(fā)中,返回錯誤碼是最常見的錯誤處理方式。C語言沒有內(nèi)建的異常機制,所以程序員必須通過檢查每個函數(shù)調(diào)用的返回值來手動處理錯誤。對于一些簡單的錯誤,返回錯誤碼通常足夠。但對于較復(fù)雜的應(yīng)用程序,錯誤碼的使用可能變得冗長且難以維護。
在一些非常嚴重的錯誤情況下(如內(nèi)存分配失敗、文件操作失敗等),開發(fā)者有時會選擇直接終止程序。例如,在發(fā)現(xiàn)內(nèi)存分配失敗時,程序可能無法繼續(xù)執(zhí)行,這時直接通過exit()
或其他方式終止程序可以避免進一步的錯誤。
2. C++異常概念??
C++的異常機制是一種專門用于處理錯誤和特殊情況的機制,可以在程序運行時中斷當(dāng)前的控制流,并跳轉(zhuǎn)到一個可以處理該錯誤的代碼塊。這種機制使得程序能夠優(yōu)雅地應(yīng)對各種錯誤,而不是像C語言那樣依賴返回錯誤碼或直接終止程序。
2.1 C++異常的基本概念
異常 (Exception)
- 異常是指程序在運行時遇到的一種錯誤或意外情況,這種情況可能會導(dǎo)致程序無法正常繼續(xù)執(zhí)行。
- 異常通過
throw
關(guān)鍵字拋出,表示錯誤已經(jīng)發(fā)生。 - 異??梢允侨我獾腃++對象(如數(shù)字、字符串、自定義類對象),但通常建議使用標(biāo)準(zhǔn)庫中的異常類(如
std::exception
及其派生類)。
異常處理機制 C++通過三種關(guān)鍵字實現(xiàn)異常處理:
try
:定義一個代碼塊,用于包含可能發(fā)生異常的代碼。throw
:在異常發(fā)生時,用于拋出異常。catch
:捕獲異常,并定義如何處理它。
2.2異常的拋出和匹配原則
1. 異常是通過拋出對象而引發(fā)的,該對象的類型決定了應(yīng)該激活哪個catch的處理代碼。
2. 被選中的處理代碼是調(diào)用鏈中與該對象類型匹配且離拋出異常位置最近的那一個。
3. 拋出異常對象后,會生成一個異常對象的拷貝,因為拋出的異常對象可能是一個臨時對象,
所以會生成一個拷貝對象,這個拷貝的臨時對象會在被catch以后銷毀。(這里的處理類似
于函數(shù)的傳值返回)
4. catch(...)可以捕獲任意類型的異常,問題是不知道異常錯誤是什么。
5. 實際中拋出和捕獲的匹配原則有個例外,并不都是類型完全匹配,可以拋出的派生類對象,
使用基類捕獲,這個在實際中非常實用,我們后面會詳細講解這個。
在函數(shù)調(diào)用鏈中異常棧展開匹配原則
1. 首先檢查throw本身是否在try塊內(nèi)部,如果是再查找匹配的catch語句。如果有匹配的,則
調(diào)到catch的地方進行處理。
2. 沒有匹配的catch則退出當(dāng)前函數(shù)棧,繼續(xù)在調(diào)用函數(shù)的棧中進行查找匹配的catch。
3. 如果到達main函數(shù)的棧,依舊沒有匹配的,則終止程序。上述這個沿著調(diào)用鏈查找匹配的
catch子句的過程稱為棧展開。所以實際中我們最后都要加一個catch(...)捕獲任意類型的異
常,否則當(dāng)有異常沒捕獲,程序就會直接終止。
4. 找到匹配的catch子句并處理以后,會繼續(xù)沿著catch子句后面繼續(xù)執(zhí)行。
代碼案例:
double Division(int a, int b) { // 當(dāng)b == 0時拋出異常 if (b == 0) throw "Division by zero condition!"; else return ((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; } catch(...){ cout<<"unkown exception"<<endl; } return 0; }
這段代碼展示了如何在 C++ 中使用異常處理機制來管理錯誤(特別是除以零的錯誤)。通過使用 throw
和 catch
,程序可以優(yōu)雅地處理異常,避免崩潰并提供友好的錯誤信息。雖然當(dāng)前的異常處理簡單直觀,但可以進一步改進,采用標(biāo)準(zhǔn)異常類和更多輸入驗證,以增強代碼的健壯性和可維護性。
2.3 異常的重新拋出
C++ 異常處理機制中的 異常重新拋出(rethrow)。當(dāng)捕獲到異常時,如果當(dāng)前函數(shù)無法處理異常,或希望更上層的函數(shù)處理異常,可以使用 throw;
語句重新拋出異常。這種機制允許在當(dāng)前 catch
塊中處理一些必要的操作(如資源釋放),然后將異常傳遞給調(diào)用鏈中的更外層函數(shù)進行進一步處理。
代碼分析
1. Division
函數(shù)
該函數(shù)實現(xiàn)了兩個整數(shù)的除法操作:
- 輸入:兩個整數(shù)
a
和b
。 - 功能:如果
b == 0
,拋出一個異常,提示 "除以零錯誤"。否則,執(zhí)行除法并返回結(jié)果。
double Division(int a, int b) { if (b == 0) // 檢查除數(shù)是否為零 throw "Division by zero condition!"; // 拋出異常 return (double)a / (double)b; // 返回除法結(jié)果 }
2. Func
函數(shù)
該函數(shù)執(zhí)行除法操作并演示異常的重新拋出。主要有以下幾個步驟:
- 輸入:從用戶讀取兩個整數(shù)
len
和time
。 - 功能:調(diào)用
Division
函數(shù)進行除法計算。如果發(fā)生除以零錯誤,則捕獲異常并執(zhí)行一些清理操作(如釋放動態(tài)分配的內(nèi)存)。
catch (...)
捕獲所有類型的異常。
delete[] array;
釋放在 Func
函數(shù)中動態(tài)分配的內(nèi)存。
然后,throw;
重新拋出異常,將其傳遞到 main
函數(shù)中的 catch
塊處理。
void Func() { int* array = new int[10]; // 動態(tài)分配內(nèi)存 try { int len, time; cin >> len >> time; // 從用戶輸入獲取兩個整數(shù) cout << Division(len, time) << endl; // 調(diào)用Division進行除法操作 } catch (...) { // 捕獲所有異常 cout << "delete []" << array << endl; delete[] array; // 釋放內(nèi)存 throw; // 重新拋出異常 } // 在異常未發(fā)生時釋放內(nèi)存 cout << "delete []" << array << endl; delete[] array; }
3. main
函數(shù)
main
函數(shù)包含了一個 try-catch
塊,負責(zé)捕獲從 Func
函數(shù)傳遞上來的異常:
- 捕獲
const char*
類型的異常(即Division by zero condition!
異常)。 - 打印異常信息。
int main() { try { Func(); // 調(diào)用Func執(zhí)行操作 } catch (const char* errmsg) { // 捕獲字符串類型的異常 cout << errmsg << endl; // 打印異常信息 } return 0; }
工作流程與邏輯
- 執(zhí)行
main
函數(shù),調(diào)用Func()
。 - 在
Func
中:- 從用戶輸入
len
和time
,調(diào)用Division(len, time)
執(zhí)行除法。 - 如果
time == 0
,Division
會拋出"Division by zero condition!"
異常。 catch (...)
捕獲該異常,并執(zhí)行異常處理:- 打印正在釋放的動態(tài)數(shù)組
array
。 - 釋放內(nèi)存(
delete[] array
)。 - 使用
throw;
重新拋出異常,將其交給main
函數(shù)的catch
塊處理。 - 在
main
中: - 捕獲到異常
"Division by zero condition!"
,并打印該錯誤信息。
- 從用戶輸入
異常的重新拋出
throw;
的作用
- 重新拋出捕獲的異常:
throw;
可以將當(dāng)前捕獲的異常重新拋出,并將其傳遞給調(diào)用鏈中的更外層catch
塊進行處理。這使得異??梢员簧蠈雍瘮?shù)進一步處理。 - 異常傳遞:在
catch
中進行一些必要的處理(如資源釋放、日志記錄等)后,可以選擇將異常交給外層的catch
塊進行后續(xù)處理。
重新拋出場景
- 資源清理:在
catch
中執(zhí)行資源釋放、日志記錄等操作后,再將異常傳遞給更外層的函數(shù)處理。 - 中斷異常傳播:有時候,
catch
只需要處理一些臨時的錯誤或簡單的資源清理工作,而不關(guān)心異常的最終處理,使用throw;
可以讓更外層的catch
塊進行實際的錯誤處理。 - 分層處理:復(fù)雜的錯誤處理可以分層執(zhí)行。內(nèi)層函數(shù)先進行一些清理工作,而具體的錯誤處理和恢復(fù)操作交給外層函數(shù)。
總結(jié)
- 異常重新拋出(
throw;
)是 C++ 異常處理機制中的一種非常有用的功能。它允許捕獲的異常在某些情況下繼續(xù)傳遞到上層函數(shù),讓更上層的代碼處理實際的錯誤。 - 在本例中,
catch
塊處理了一些臨時的資源清理工作,然后使用throw;
將異常重新拋出,最終由main
函數(shù)中的catch
塊捕獲并輸出異常信息。 - 異常處理中的資源管理(例如內(nèi)存釋放)非常重要,因為它確保即使發(fā)生異常,動態(tài)分配的資源也能夠正確釋放,避免內(nèi)存泄漏等問題。
這種機制可以使異常處理更具靈活性,適應(yīng)不同層級的錯誤處理需求,同時保證資源的有效釋放。
2.4 異常安全
在 C++ 中,異常安全指的是在出現(xiàn)異常時,確保程序資源管理正確、數(shù)據(jù)一致性不被破壞。構(gòu)造函數(shù)和析構(gòu)函數(shù)是異常安全的關(guān)鍵點:
- 構(gòu)造函數(shù):避免在構(gòu)造函數(shù)中拋出異常,否則對象可能不完全初始化。使用臨時對象或函數(shù)來避免復(fù)雜操作中出現(xiàn)異常。
- 析構(gòu)函數(shù):盡量不要在析構(gòu)函數(shù)中拋出異常,因為這可能導(dǎo)致資源泄漏或程序崩潰。如果必須處理異常,捕獲并避免傳播。
- 資源管理中的異常:在動態(tài)內(nèi)存分配(
new
)和鎖管理中,異常可能導(dǎo)致內(nèi)存泄漏或死鎖。 - AII(資源獲取即初始化)模式能自動管理資源,確保異常發(fā)生時正確釋放資源。
- 智能指針:使用智能指針(如
std::unique_ptr
)可以自動管理內(nèi)存,避免內(nèi)存泄漏。 - RAII 和智能指針是解決異常安全問題的重要工具,它們通過在對象生命周期內(nèi)自動管理資源來確保代碼的穩(wěn)定性。
2.5 異常規(guī)范
1. 異常規(guī)格說明的目的是為了讓函數(shù)使用者知道該函數(shù)可能拋出的異常有哪些。 可以在函數(shù)的
后面接throw(類型),列出這個函數(shù)可能拋擲的所有異常類型。
2. 函數(shù)的后面接throw(),表示函數(shù)不拋異常。
3. 若無異常接口聲明,則此函數(shù)可以拋擲任何類型的異常。
// 這里表示這個函數(shù)會拋出A/B/C/D中的某種類型的異常 void fun() throw(A,B,C,D); // 這里表示這個函數(shù)只會拋出bad_alloc的異常 void* operator new (std::size_t size) throw (std::bad_alloc); // 這里表示這個函數(shù)不會拋出異常 void* operator delete (std::size_t size, void* ptr) throw(); // C++11 中新增的noexcept,表示不會拋異常 thread() noexcept; thread (thread&& x) noexcept;
3. 自定義異常體系 ????
在 C++ 中,自定義異常體系指的是定義自己的異常類,用于表示特定類型的錯誤或異常情況。這樣可以使異常處理更具可讀性和靈活性,方便區(qū)分不同類型的錯誤。
3.1 自定義異常類
自定義異常類通常繼承自標(biāo)準(zhǔn)庫中的 std::exception
類,或者更常見的是直接繼承自 std::runtime_error
或 std::logic_error
。
基本步驟:
- 繼承
std::exception
或其子類:自定義異常類應(yīng)繼承自std::exception
或std::runtime_error
,并重寫構(gòu)造函數(shù)來傳遞錯誤消息。 - 添加構(gòu)造函數(shù)和成員函數(shù):可以添加自定義的構(gòu)造函數(shù),用來初始化異常信息。
#include <iostream> #include <stdexcept> #include <string> // 自定義異常類,繼承自 std::runtime_error class MyException : public std::runtime_error { public: MyException(const std::string& message) : std::runtime_error(message) {} // 調(diào)用基類構(gòu)造函數(shù) }; void testFunction() { throw MyException("Something went wrong!"); } int main() { try { testFunction(); } catch (const MyException& e) { std::cout << "Caught MyException: " << e.what() << std::endl; } catch (const std::exception& e) { std::cout << "Caught std::exception: " << e.what() << std::endl; } return 0; }
3.2 自定義多個異常類型
可以創(chuàng)建不同的異常類型來表示不同的錯誤情境。每種異常類型可以有不同的錯誤信息,或不同的邏輯處理
class FileNotFoundException : public std::runtime_error { public: FileNotFoundException(const std::string& filename) : std::runtime_error("File not found: " + filename), filename(filename) {} const std::string& getFilename() const { return filename; } private: std::string filename; }; class InvalidInputException : public std::invalid_argument { public: InvalidInputException(const std::string& message) : std::invalid_argument(message) {} }; // 使用示例 try { throw FileNotFoundException("data.txt"); } catch (const FileNotFoundException& e) { std::cout << e.what() << " Filename: " << e.getFilename() << std::endl; }
3.3. 捕獲自定義異常
在 try-catch
塊中捕獲自定義異常時,應(yīng)該使用適當(dāng)?shù)漠惓n愋筒东@,確保正確處理特定的錯誤。
try { throw InvalidInputException("Invalid input provided"); } catch (const InvalidInputException& e) { std::cout << "Caught InvalidInputException: " << e.what() << std::endl; } catch (const std::exception& e) { std::cout << "Caught a generic exception: " << e.what() << std::endl; }
3.4 異常層次結(jié)構(gòu)設(shè)計
- 基類:
std::exception
或std::runtime_error
用作基類,定義通用的接口(如what()
)。 - 子類:可以為每種具體錯誤類型創(chuàng)建子類,繼承基類并擴展功能。
這種設(shè)計方式使得異常體系更加清晰、可擴展。
總結(jié)
自定義異常體系的核心是通過繼承現(xiàn)有的異常類(如 std::exception
或 std::runtime_error
)來創(chuàng)建符合特定需求的異常類型。這樣可以方便地進行異常區(qū)分,幫助編寫更具表現(xiàn)力和可維護性的代碼。
4.C++標(biāo)準(zhǔn)庫的異常體系 ????
C++ 提供了一系列標(biāo)準(zhǔn)的異常,定義在 中,我們可以在程序中使用這些標(biāo)準(zhǔn)的異常。它們是以父子類層次結(jié)構(gòu)組織起來的,如下所示:
說明:實際中我們可以可以去繼承exception類實現(xiàn)自己的異常類。但是實際中很多公司像上面一樣自己定義一套異常繼承體系。因為C++標(biāo)準(zhǔn)庫設(shè)計的不夠好用
int main() { try{ vector<int> v(10, 5); // 這里如果系統(tǒng)內(nèi)存不夠也會拋異常 v.reserve(1000000000); // 這里越界會拋異常 v.at(10) = 100; } catch (const exception& e) // 這里捕獲父類對象就可以 { cout << e.what() << endl; } catch (...) { cout << "Unkown Exception" << endl; } return 0; }
5.異常的優(yōu)缺點????
C++異常的優(yōu)點:
1. 異常對象定義好了,相比錯誤碼的方式可以清晰準(zhǔn)確的展示出錯誤的各種信息,甚至可以包
含堆棧調(diào)用的信息,這樣可以幫助更好的定位程序的bug。
2. 返回錯誤碼的傳統(tǒng)方式有個很大的問題就是,在函數(shù)調(diào)用鏈中,深層的函數(shù)返回了錯誤,那
么我們得層層返回錯誤,最外層才能拿到錯誤,具體看下面的詳細解釋。
// 1.下面這段偽代碼我們可以看到ConnnectSql中出錯了,先返回給ServerStart, ServerStart再返回給main函數(shù),main函數(shù)再針對問題處理具體的錯誤。 // 2.如果是異常體系,不管是ConnnectSql還是ServerStart及調(diào)用函數(shù)出錯,都不用檢查,因 為拋出的異常異常會直接跳到main函數(shù)中catch捕獲的地方,main函數(shù)直接處理錯誤。 int ConnnectSql() { // 用戶名密碼錯誤 if (...) return 1; // 權(quán)限不足 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. 部分函數(shù)使用異常更好處理,比如構(gòu)造函數(shù)沒有返回值,不方便使用錯誤碼方式處理。比如
T& operator這樣的函數(shù),如果pos越界了只能使用異?;蛘呓K止程序處理,沒辦法通過返回
值表示錯誤。
C++異常的缺點:
1. 異常會導(dǎo)致程序的執(zhí)行流亂跳,并且非常的混亂,并且是運行時出錯拋異常就會亂跳。這會
導(dǎo)致我們跟蹤調(diào)試時以及分析程序時,比較困難。
2. 異常會有一些性能的開銷。當(dāng)然在現(xiàn)代硬件速度很快的情況下,這個影響基本忽略不計。
3. C++沒有垃圾回收機制,資源需要自己管理。有了異常非常容易導(dǎo)致內(nèi)存泄漏、死鎖等異常
安全問題。這個需要使用RAII來處理資源的管理問題。學(xué)習(xí)成本較高。
4. C++標(biāo)準(zhǔn)庫的異常體系定義得不好,導(dǎo)致大家各自定義各自的異常體系,非常的混亂。
5. 異常盡量規(guī)范使用,否則后果不堪設(shè)想,隨意拋異常,外層捕獲的用戶苦不堪言。所以異常
規(guī)范有兩點:
- 一、拋出異常類型都繼承自一個基類。
- 二、函數(shù)是否拋異常、拋什么異常,都使用 func() throw();的方式規(guī)范化。
總結(jié):異??傮w而言,利大于弊,所以工程中我們還是鼓勵使用異常的。另外OO的語言基本都是用異常處理錯誤,這也可以看出這是大勢所趨。
到此這篇關(guān)于C++ 異常處理機制與自定義異常體系的文章就介紹到這了,更多相關(guān)C++ 異常處理機制內(nèi)容請搜索腳本之家以前的文章或繼續(xù)瀏覽下面的相關(guān)文章希望大家以后多多支持腳本之家!
相關(guān)文章
數(shù)據(jù)結(jié)構(gòu)之AVL樹詳解
這篇文章主要介紹了數(shù)據(jù)結(jié)構(gòu)之AVL樹詳解,本文非常細致的講解了AVL樹的基礎(chǔ)知識、AVL樹的旋轉(zhuǎn)操作、AVL數(shù)的插入和刪除操作等,需要的朋友可以參考下2014-08-08sqlserver,sqlite,access數(shù)據(jù)庫鏈接字符串整理
本節(jié)主要整理sqlserver,sqlite,access數(shù)據(jù)庫鏈接字符串,有需要的朋友可以參考下2014-07-07C語言實現(xiàn)Linux下的socket文件傳輸實例
這篇文章主要介紹了C語言實現(xiàn)Linux下的socket文件傳輸?shù)姆椒?較為詳細的分析了C語言文件Socket文件傳輸客戶端與服務(wù)器端相關(guān)實現(xiàn)技巧,需要的朋友可以參考下2015-06-06c++實現(xiàn)十進制轉(zhuǎn)換成16進制示例
這篇文章主要介紹了c++實現(xiàn)十進制轉(zhuǎn)換成16進制示例,需要的朋友可以參考下2014-05-05深入Windows下的回車是回車換行(\r\n)還是換行回車(\n\r)的詳解
本篇文章對Windows下的回車是回車換行(\r\n)還是換行回車(\n\r)進行了詳細的分析介紹,需要的朋友參考下2013-05-05