@TOC
C++異常處理:底層邏輯、實操方法與實戰技巧
一、背景知識
1.1 C語言的錯誤處理侷限
C語言處理錯誤的核心方式僅有兩種,且都存在顯著短板:
- 直接終止程序:像
assert斷言這類方式,一旦檢測到非法條件(比如數組越界、空指針訪問)就直接終止程序。這種方式對用户極不友好,比如僅因一個參數錯誤就導致整個應用崩潰,完全沒有容錯空間。 - 返回錯誤碼:多數C語言庫會將錯誤信息存入全局變量
errno,但單一錯誤碼無法精準描述問題場景——比如同樣是errno=2,可能是文件不存在,也可能是路徑錯誤,排查時需要額外加大量日誌才能定位問題。
1.2 C++異常的核心邏輯
異常是C++針對C語言錯誤處理痛點設計的靈活機制:當函數遇到自身無法解決的錯誤時,可主動拋出異常,由調用鏈中更上層的函數處理。它的核心依賴三個關鍵字:
throw:函數遇到無法處理的錯誤時,用該關鍵字拋出具體的錯誤信息(支持任意類型)。catch:捕獲throw拋出的異常,根據異常類型做針對性處理,可搭配多個catch處理不同類型異常。try:必須包裹可能拋出異常的代碼塊,且需與catch配套使用,throw只能出現在try塊內或try塊調用的函數中。
二、異常的實際使用
2.1 基礎使用示例
異常的核心是try-throw-catch的配合,我們以“計算矩形面積”為例(邊長為負數時拋出異常),替代傳統的除法除0場景,更貼近日常開發:
// 計算矩形面積:邊長為負數時拋出異常
double CalcRectArea(double width, double height) {
if (width <= 0 || height <= 0) {
// 拋出字符串類型異常,描述具體錯誤
throw "矩形邊長不能為負數或0!";
}
return width * height;
}
void CalcAreaFunc() {
double w, h;
cout << "請輸入矩形的寬和高:" << endl;
cin >> w >> h;
try {
cout << "矩形面積:" << CalcRectArea(w, h) << endl;
}
// 先捕獲int類型異常(示例用,本場景不會觸發)
catch (int errCode) {
cout << "錯誤碼:" << errCode << endl;
}
cout << "CalcAreaFunc函數執行結束" << endl;
}
int main() {
while (1) {
try {
CalcAreaFunc();
}
// 捕獲字符串類型異常
catch (const char* errMsg) {
cout << "捕獲異常:" << errMsg << endl;
}
// 捕獲int類型異常
catch (int errCode) {
cout << "捕獲異常碼:" << errCode << endl;
}
// 兜底捕獲所有未匹配的異常
catch (...) {
cout << "捕獲到未知類型異常" << endl;
}
}
return 0;
}
2.2 異常的匹配規則
異常的捕獲和處理有明確的匹配邏輯,確保錯誤能被精準處理:
- 異常的觸發由拋出的對象類型決定,只有
catch塊的類型與異常對象類型一致,才會被激活。 - 在函數調用鏈中,系統會優先選擇離
throw最近的匹配catch塊執行。 - 拋出異常對象時,會生成一個臨時拷貝(類似函數值傳遞),該拷貝在
catch處理完成後自動銷燬。 catch(...)是“萬能捕獲”,能匹配所有類型異常,但無法獲取異常的具體信息,適合作為兜底方案。- 特殊場景:拋出派生類對象時,可通過基類類型的
catch塊捕獲,這是工程中實現異常統一管理的核心技巧。
函數調用鏈的異常匹配流程:
- 首先檢查
throw是否在try塊內,若是則查找當前try後的匹配catch,找到後執行處理邏輯,程序繼續運行。 - 若未找到匹配
catch,則退出當前函數棧,到調用該函數的上層函數中繼續查找。 - 若直到
main函數仍無匹配catch,程序會直接終止。因此實際開發中,建議在main函數中添加catch(...)兜底。
2.3 拋出自定義異常對象
字符串類型異常的信息有限,實際開發中常自定義異常類,封裝更多錯誤信息(比如錯誤碼、錯誤場景):
// 自定義基礎異常類
class BaseException {
public:
BaseException(const string& msg, int code)
: _errMsg(msg)
, _errCode(code) {}
// 虛函數,方便派生類重寫
virtual string GetErrInfo() const {
return _errMsg;
}
int GetErrCode() const {
return _errCode;
}
protected:
string _errMsg; // 錯誤描述
int _errCode; // 錯誤碼
};
// 計算圓形面積,半徑非法時拋出自定義異常
double CalcCircleArea(double radius) {
if (radius <= 0) {
// 拋出自定義異常對象(錯誤碼1001:參數非法)
throw BaseException("圓形半徑不能為負數或0", 1001);
}
return 3.14159 * radius * radius;
}
void CalcCircleFunc() {
double r;
cout << "請輸入圓形半徑:" << endl;
cin >> r;
cout << "圓形面積:" << CalcCircleArea(r) << endl;
}
int main() {
while (1) {
try {
CalcCircleFunc();
}
// 捕獲自定義異常(用引用避免拷貝,更高效)
catch (const BaseException& e) {
cout << "錯誤信息:" << e.GetErrInfo()
<< " | 錯誤碼:" << e.GetErrCode() << endl;
}
// 兜底捕獲
catch (...) {
cout << "未知異常,請檢查輸入" << endl;
}
}
return 0;
}
2.4 異常的重新拋出
實際開發中,有時需要對異常做“局部處理+向上拋”,比如讀取配置文件失敗時重試,重試失敗再向上層反饋:
// 繼承基礎異常類,擴展文件異常信息
class FileException : public BaseException {
public:
FileException(const string& msg, int code, const string& fileName)
: BaseException(msg, code)
, _fileName(fileName) {}
// 重寫錯誤信息函數,補充文件名
virtual string GetErrInfo() const override {
string info = "文件異常-";
info += _fileName;
info += ":";
info += _errMsg;
return info;
}
private:
string _fileName; // 出錯的文件名
};
// 模擬讀取配置文件,隨機觸發異常
void ReadConfigFile(const string& fileName) {
srand(time(0));
// 30%概率觸發“文件不存在”異常
if (rand() % 10 < 3) {
throw FileException("文件不存在", 2001, fileName);
}
// 20%概率觸發“權限不足”異常
else if (rand() % 10 < 2) {
throw FileException("文件讀取權限不足", 2002, fileName);
}
cout << fileName << "配置文件讀取成功!" << endl;
}
// 讀取配置文件,失敗時重試3次
void LoadConfig() {
string fileName = "app_config.ini";
int retryTimes = 3; // 重試次數
while (retryTimes--) {
try {
ReadConfigFile(fileName);
break; // 讀取成功,退出重試
}
catch (const BaseException& e) {
// 僅“文件不存在”且還有重試次數時,繼續重試
if (e.GetErrCode() == 2001 && retryTimes > 0) {
cout << "重試讀取(" << retryTimes << "次剩餘)..." << endl;
continue;
}
// 其他錯誤或重試耗盡,重新拋出異常
throw e;
}
}
}
2.5 異常安全注意事項
異常會導致程序執行流跳轉,若處理不當易引發資源泄漏(比如內存未釋放、文件未關閉),需注意以下幾點:
- 構造函數儘量避免拋異常:構造函數負責初始化對象,若中途拋異常,對象可能只初始化一半,成為“不完整對象”。
- 析構函數禁止拋異常:析構函數負責釋放資源,若析構時拋異常,可能導致資源(如文件句柄、內存)無法正常釋放。
- 異常場景下的資源管理:使用
new分配內存後,若拋出異常未執行delete,會造成內存泄漏,可手動釋放或用RAII(如智能指針)簡化管理。
示例:異常場景下確保動態內存釋放:
double CalcTriangleArea(double base, double height) {
if (base <= 0 || height <= 0) {
throw "三角形底或高不能為負數/0!";
}
return 0.5 * base * height;
}
void ProcessTriangleData() {
// 動態分配兩個數組
double* dataBuf1 = new double[10];
double* dataBuf2 = new double[10];
double b, h;
cout << "請輸入三角形底和高:" << endl;
cin >> b >> h;
try {
cout << "三角形面積:" << CalcTriangleArea(b, h) << endl;
}
catch (...) {
// 異常發生時,先釋放內存再重新拋出
delete[] dataBuf1;
delete[] dataBuf2;
throw; // 向上層傳遞異常
}
// 正常執行時釋放內存
delete[] dataBuf1;
delete[] dataBuf2;
}
三、自定義異常體系
C++標準庫的exception類功能簡單,無法滿足工程級的異常管理需求。多數團隊會搭建“基礎異常+業務異常”的繼承體系,統一異常處理邏輯,避免異常類型混亂。
以下是模擬日常開發的異常體系示例(覆蓋文件、數據庫、緩存場景):
#include <iostream>
#include <string>
#include <ctime> // 替換time.h為C++標準頭文件ctime
#include <windows.h> // 解決Sleep標識符找不到的問題(Windows平台)
// 如果需要跨平台,可替換Sleep為:
// #include <chrono>
// #include <thread>
using namespace std;
// 自定義基礎異常類
class BaseException {
public:
BaseException(const string& msg, int code)
: _errMsg(msg), _errCode(code) {}
virtual string GetErrInfo() const {
return _errMsg;
}
int GetErrCode() const {
return _errCode;
}
protected:
string _errMsg;
int _errCode;
};
// 1. 文件異常(繼承基礎類)
class FileException : public BaseException {
public:
FileException(const string& msg, int code, const string& file)
: BaseException(msg, code), _filePath(file) {}
virtual string GetErrInfo() const override {
// 修復可能的編碼/字符問題,統一用英文冒號(避免全角符號)
return "FileErr[" + to_string(_errCode) + "]: " + _filePath + "-" + _errMsg;
}
private:
string _filePath;
};
// 2. 數據庫異常(繼承基礎類)
class DbException : public BaseException {
public:
DbException(const string& msg, int code, const string& sql)
: BaseException(msg, code), _sqlStmt(sql) {}
virtual string GetErrInfo() const override {
return "DbErr[" + to_string(_errCode) + "]: " + _sqlStmt + "-" + _errMsg;
}
private:
string _sqlStmt;
};
// 3. 緩存異常(繼承基礎類)
class CacheException : public BaseException {
public:
CacheException(const string& msg, int code)
: BaseException(msg, code) {}
virtual string GetErrInfo() const override {
return "CacheErr[" + to_string(_errCode) + "]: " + _errMsg;
}
};
// 模擬業務函數:緩存操作
void AccessCache() {
// 修復警告C4244:強制轉換time_t到unsigned int(srand要求unsigned int參數)
srand((unsigned int)time(nullptr));
if (rand() % 5 == 0) {
throw CacheException("緩存鍵不存在", 3001);
}
else if (rand() % 6 == 0) {
throw CacheException("緩存連接超時", 3002);
}
cout << "緩存讀取成功" << endl;
}
// 模擬業務函數:數據庫操作
void ExecDbSql() {
// 修復警告C4244:強制類型轉換
srand((unsigned int)time(nullptr));
if (rand() % 7 == 0) {
throw DbException("SQL執行權限不足", 4001, "select * from user where id=1");
}
AccessCache(); // 調用緩存函數
}
// 模擬業務函數:配置加載(調用數據庫)
void LoadAppConfig() {
// 修復警告C4244:強制類型轉換
srand((unsigned int)time(nullptr));
if (rand() % 4 == 0) {
throw FileException("配置文件損壞", 2003, "./config/app.ini");
}
ExecDbSql(); // 調用數據庫函數
}
// 主函數統一捕獲異常
int main() {
while (1) {
// Sleep是Windows API,需包含windows.h;如果是Linux/Mac,替換為:
// this_thread::sleep_for(chrono::seconds(1));
Sleep(1000); // 暫停1秒,避免輸出過快
try {
LoadAppConfig();
}
// 多態捕獲所有派生類異常
catch (const BaseException& e) {
cout << "捕獲業務異常:" << e.GetErrInfo() << endl;
}
// 兜底捕獲未知異常
catch (...) {
cout << "捕獲未定義異常" << endl;
}
}
return 0;
}
四、異常的優缺點
4.1 核心優勢
- 錯誤信息更豐富:自定義異常類可封裝錯誤碼、錯誤場景、調用路徑等信息,相比錯誤碼能快速定位問題。
- 適配特殊函數場景:構造函數無返回值、
operator[]等運算符無法通過返回值標識錯誤,異常是這類場景的最優解。 - 兼容主流第三方庫:boost、Google Test(gtest)、GMock等常用庫均基於異常機制設計,使用這些庫必須適配異常。
4.2 主要不足
- 執行流不直觀:異常會跳轉到對應的
catch塊,打斷線性執行流程,調試時可能跳過斷點,增加問題排查難度。 - 輕微性能開銷:異常的拋出和捕獲會涉及棧展開、對象拷貝等操作,雖現代CPU下影響極小,但高頻調用場景需謹慎。
- 資源泄漏風險:C++無垃圾回收機制,異常可能跳過
delete/close等資源釋放語句,需依賴RAII(如智能指針、文件流)規避。 - 標準庫體系不完善:C++標準異常體系設計簡單,各團隊需自定義體系,導致不同項目的異常處理邏輯不統一。