目錄
C語言處理錯誤的方式
C++異常的概念
C++異常的使用
異常的拋出與捕獲匹配原則
函數調用鏈中的棧展開?
異常重新拋出?
異常安全
異常規范
標準庫異常體系
自定義異常體系
異常的優缺點
C語言處理錯誤的方式
-
返回值檢查:函數返回特定錯誤碼或值標識失敗,但需逐層檢查且易被忽略。
-
全局變量?
errno
:依賴全局變量記錄錯誤類型,存在線程安全隱患和覆蓋風險。 -
斷言(Assert):通過斷言驗證邏輯假設,但僅適用于調試且失敗直接終止程序。
-
非局部跳轉(setjmp/longjmp):支持跨函數錯誤跳轉,但易導致資源泄漏和代碼混亂。
-
信號處理:捕獲系統信號處理嚴重錯誤,但處理函數功能受限且不可靠。
-
Goto清理:集中釋放資源避免冗余,但濫用會破壞代碼結構化邏輯。
C++異常的概念
C++ 異常處理是一種用于管理程序運行時錯誤的機制,它通過分離錯誤處理代碼和正常邏輯來提高代碼的可維護性。?
-
try
:包裹可能拋出異常的代碼塊 -
throw
:拋出異常對象(任意類型) -
catch
:捕獲并處理特定類型的異常
try {// 可能拋出異常的代碼if (error) throw MyException("Error occurred");
}
catch (const MyException& e) {// 處理 MyException 類型異常std::cerr << e.what() << std::endl;
}
catch (...) { // 捕獲所有異常std::cerr << "Unknown error" << std::endl;
}
C++異常的使用
異常的拋出與捕獲匹配原則
1、類型精確匹配
-
異常捕獲基于?類型匹配,
catch
?塊按順序嘗試匹配異常類型 - 被選中的處理代碼(catch塊)是調用鏈中與該對象類型匹配且離拋出異常位置最近的那一個。
- 異常是通過拋出對象而引發的,該對象的類型決定了應該激活哪個catch的處理代碼,如果拋出的異常對象沒有捕獲,或是沒有匹配類型的捕獲,那么程序會終止報錯。
try {throw 42; // 拋出 int 類型異常
}
catch (double d) { /* 不會捕獲 */ }
catch (int i) { // 匹配成功std::cout << "Caught int: " << i;
}
2、繼承體系中的匹配
-
基類?
catch
?塊可以捕獲派生類異常(需通過?引用或指針?捕獲避免對象切片) - 捕獲和拋出的異常類型并不一定要完全匹配,可以拋出派生類對象,使用基類進行捕獲。
-
推薦實踐:優先捕獲派生類異常,再捕獲基類
try {throw std::runtime_error("Error");
}
catch (const std::runtime_error& e) { // 優先匹配具體類型std::cerr << "Runtime error: " << e.what();
}
catch (const std::exception& e) { // 基類捕獲兜底std::cerr << "Standard exception: " << e.what();
}
3、特殊匹配規則
-
catch (...)
?捕獲所有異常(通常用于資源清理),但捕獲后無法知道異常錯誤是什么。 -
const
?修飾不影響匹配:catch (std::exception)
?與?catch (const std::exception)
?視為相同
函數調用鏈中的棧展開?
- 當異常被拋出后,首先檢查 throw 本身是否在try塊內部,如果在則查找匹配的catch語句,如果有匹配的,則跳到catch的地方進行處理。
- 如果當前函數棧沒有匹配的 catch 則退出當前函數棧,繼續在上一個調用函數棧中進行查找匹配的catch。找到匹配的catch子句并處理以后,會沿著 catch 子句后面繼續執行,而不會跳回到原來拋異常的地方。
- 如果到達main函數的棧,依舊沒有找到匹配的catch,則終止程序。
void func3()
{std::vector<int> localObj(100); // RAII 對象throw std::runtime_error("Boom"); // 拋出異常// localObj 自動析構
}void func2() { func3(); } // 異常繼續傳播
void func1() { func2(); } // 異常繼續傳播int main()
{try {func1();}catch (const std::exception& e) {std::cerr << "Caught: " << e.what();}return 0;
}
-
函數調用鏈:
-
main()
?調用?func1()
-
func1()
?調用?func2()
-
func2()
?調用?func3()
-
-
異常拋出(func3):
-
在?
func3()
?中,首先構造局部對象?std::vector<int> localObj(100)
(RAII管理內存)。 -
執行?
throw std::runtime_error("Boom")
,拋出異常,函數執行中斷。
-
-
棧展開(Stack Unwinding):
-
func3 棧幀銷毀:
localObj
?的析構函數自動調用,釋放分配的100個int內存(RAII確保資源釋放)。 -
func2 棧幀銷毀:因無局部對象,直接退出。
-
func1 棧幀銷毀:同理,無資源需清理。
-
-
異常捕獲(main):
-
異常傳播至?
main()
?的?try
?塊。 -
catch (const std::exception& e)
?捕獲異常(std::runtime_error
?是?std::exception
?的派生類)。 -
輸出錯誤信息:
Caught: Boom
。
-
異常重新拋出?
在?catch
?塊中使用?throw;
?重新拋出當前異常
典型場景:
-
記錄日志后繼續傳播異常
-
部分處理異常后交由上層處理
異常安全
- 構造函數完成對象的構造和初始化,最好不要在構造函數中拋出異常,否則可能導致對象不完整或沒有完全初始化。
- 析構函數主要完成對象資源的清理,最好不要在析構函數中拋出異常,否則可能導致資源泄露(內存泄露、句柄未關閉等)。
- C++中異常經常會導致資源泄露的問題,比如在new和delete中拋出異常,導致內存泄露,在lock和unlock之間拋出異常導致死鎖,C++經常使用RAII的方式來解決以上問題。
異常規范
1、優先使用?
noexcept
-
適用場景:
-
移動操作、析構函數、內存釋放函數(如?
operator delete
)。 -
明確無失敗可能的函數(如數學計算)。
-
double sqrt(double x) noexcept
{ // 假設輸入已校驗,不會拋異常return std::sqrt(x);
}
2. 避免使用動態異常聲明
void oldFunc() throw(std::runtime_error); // C++17 已移除,禁止使用
// 替代方案:通過文檔說明可能拋出的異常類型。
3、注意事項?
- 在函數的后面接
throw(type1, type2, ...)
,列出這個函數可能拋擲的所有異常類型。 - 在函數的后面接
throw()
或noexcept
(C++11),表示該函數不拋異常。 - 若無異常接口聲明,則此函數可以拋擲任何類型的異常。(異常接口聲明不是強制的)
// 表示func函數可能會拋出A/B/C/D類型的異常
void func() 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();
標準庫異常體系
C++ 標準庫提供了一套層次化的異常類體系,所有標準異常均繼承自?std::exception
?基類。這些異常類型覆蓋了常見的程序錯誤場景,開發者可以直接使用或繼承它們實現自定義異常。
std::exception
├── std::bad_alloc // 內存分配失敗(new 失敗)
├── std::bad_cast // dynamic_cast 轉換失敗(非多態類型)
├── std::bad_typeid // typeid 操作符作用于空指針
├── std::ios_base::failure // I/O 流錯誤(如文件打開失敗)
|
├── std::logic_error // 程序邏輯錯誤(可預防的)
│ ├── std::invalid_argument // 無效參數(如參數不符合預期范圍)
│ ├── std::domain_error // 數學運算定義域錯誤(如對負數取對數)
│ ├── std::length_error // 超出允許長度(如 vector::reserve 超過 max_size)
│ └── std::out_of_range // 訪問越界(如 vector::at 越界索引)
|
└── std::runtime_error // 運行時錯誤(不可預見的)├── std::range_error // 計算結果超出有效范圍(如浮點數轉換溢出)├── std::overflow_error // 算術上溢錯誤├── std::underflow_error // 算術下溢錯誤└── std::system_error // 系統調用錯誤(含錯誤碼,C++11 引入)
- exception類的 what成員函數 和 析構函數都定義成了虛函數,方便子類對其進行重寫,從而達到多態的效果。
- 我們也可以去繼承exception類來實現自己的異常類,但實際中很多公司都會自己定義一套異常繼承體系。
?自定義異常:通過繼承?std::runtime_error
?或?std::logic_error
?添加額外信息。
#include <stdexcept>
#include <string>class NetworkException : public std::runtime_error
{int error_code_;
public:NetworkException(int code, const std::string& message): std::runtime_error(message), error_code_(code) {}int getErrorCode() const noexcept { return error_code_; }
};// 使用
throw NetworkException(404, "Service not found");
代碼示例
#include <iostream>
#include <fstream>
#include <stdexcept>
#include <string>void readConfigFile(const std::string& filename)
{std::ifstream file(filename);if (!file) {throw std::runtime_error("無法打開文件: " + filename);}std::string line;while (std::getline(file, line)) {if (line.empty()) {throw std::invalid_argument("配置文件存在空行");}// 解析配置...}
}int main()
{try {readConfigFile("settings.conf");}catch (const std::invalid_argument& e) {std::cerr << "參數錯誤: " << e.what() << std::endl;}catch (const std::runtime_error& e) {std::cerr << "運行時錯誤: " << e.what() << std::endl;}catch (const std::exception& e) {std::cerr << "標準異常: " << e.what() << std::endl;}return 0;
}
自定義異常體系
實際中很多公司都會自定義自己的異常體系進行規范的異常管理。
- 公司中的項目一般會進行模塊劃分,讓不同的程序員或小組完成不同的模塊,如果不對拋異常這件事進行規范,那么負責最外層捕獲異常的程序員就非常難受了,因為他需要捕獲大家拋出的各種類型的異常對象。
- 因此實際中都會定義一套繼承的規范體系,先定義一個最基礎的異常類,所有人拋出的異常對象都必須是繼承于該異常類的派生類對象,因為異常語法規定可以用基類捕獲拋出的派生類對象,因此最外層就只需捕獲基類就行了。
一、為何需要自定義異常?
-
錯誤分類:為特定領域(如文件I/O、網絡、數據庫)定義明確的錯誤類型。
-
攜帶額外信息:在異常對象中封裝錯誤碼、文件名、操作步驟等上下文信息。
-
統一接口:繼承自?
std::exception
,兼容標準異常處理邏輯。
二、設計原則
-
繼承標準異常:所有自定義異常應直接或間接繼承?
std::exception
。 -
層次化結構:按錯誤類型分層(如?
NetworkException
?派生出?TimeoutException
)。 -
支持多態:通過虛函數(如?
what()
)提供統一的錯誤信息接口。 -
異常安全:確保自定義異常類的構造函數和成員函數不拋出異常。
三、實現步驟
1. 基類設計(兼容標準異常)?
#include <exception>
#include <string>class Exception
{
public:// 構造函數(允許傳入錯誤描述)Exception(int errid, const char* errmsg):_errid(errid), _errmsg(errmsg){}int GetErrid() const{return _errid;}// 重寫 what(),返回錯誤信息virtual string what() const{return _errmsg;}
protected:int _errid; //錯誤編號string _errmsg; //錯誤描述//...
};
2. 派生具體異常類
// 文件操作異常
class FileIOException : public Exception
{
public:explicit FileIOException(const std::string& filename, const std::string& action): Exception("File Error: Failed to " + action + " file '" + filename + "'") {}
};// 網絡超時異常
class NetworkTimeoutException : public Exception
{
public:NetworkTimeoutException(const std::string& url, int timeout_sec): Exception("Network Timeout: Request to '" + url + "' timed out after " + std::to_string(timeout_sec) + " seconds") {}
};
?3.?使用自定義異常
void readFile(const std::string& filename)
{std::ifstream file(filename);if (!file.is_open()) {// 拋出異常throw FileIOException(filename, "open");}// 文件操作...
}try
{readFile("config.yaml");
}
catch (const FileIOException& e)
{std::cerr << "文件操作失敗: " << e.what() << std::endl;// 嘗試恢復或重試
}
catch (const MyBaseException& e)
{std::cerr << "通用錯誤: " << e.what() << std::endl;
}