C語言本身沒有處理異常的機制,通常需要通過錯誤碼(error)、assert、全局變量、函數返回值等方法處理錯誤;這種處理方法雖然邏輯直觀,但是多層調用時需逐層傳遞且無法自動清理資源,功能有限。
所以,Bjarne Stroustrup在設計C++時,為了更好地處理程序中的錯誤,將異常處理機制引入了C++,其基本思想是讓函數在發現自己無法處理的錯誤時拋出一個異常,然後由其調用者來處理這個問題 。
一.異常概念:
C++異常時專門處理程序運行時錯誤的機制,核心是將“錯誤檢測”和“錯誤處理”分離。
核心語法
try-catch-throw
- throw:“拋出異常”,在檢測到錯誤的地方使用,可拋出任意類型(如int、字符串、自定義對象)
- try:將可能拋出異常的代碼塊放在 try 內部,標記“這段代碼需要監控異常”
- catch:“捕獲並處理異常”,緊跟 try 之後,指定要處理的異常類型,只有類型匹配時才會執行(通常有多個catch)
// 舉例:模擬可能出錯的函數(參數非法時拋異常)
void checkAge(int age)
{
if (age < 0 || age > 150)
{
// throw:拋出異常(可拋int、字符串等任意類型)
throw "年齡必須在 0-150 之間";
}
cout << "年齡合法:" << age << endl;
}
int main()
{
// try:包裹「可能拋出異常」的代碼塊
try
{
checkAge(200); // 調用風險函數,會觸發異常
}
// catch:捕獲並處理異常,括號內指定要處理的異常類型
//如果throw出的異常為int類型,這個catch無法捕捉
catch (const char* errMsg)
{
cout << "捕獲異常:" << errMsg << endl; // 處理邏輯(如打印錯誤)
}
return 0;
}
catch匹配機制相關細節
- 異常由拋出對象throw引發(可拋出任意類型),處理調用鏈中異常,會匹配離throw最近的一個;一旦throw出現,會跨越函數棧,直接跳到(最近的)catch
- 對於catch它只能捕獲其參數對應類型異常,想捕獲任意類型異常,我們可以使用
catch(...)(但它無法得知具體問題,所以catch(...)一般放在最後,防止拋不規範異常等問題) ;異常類型是從上往下識別,上面都沒被識別後走catch(...)
void func()
{
//
try
{
test();
}
catch(int err){
cout<<"int"<<endl;}
catch(const char* err){
cout<<"char"<<endl;}
catch(...){
cout<<"不規範異常"<<endl;}
}
- 但在實際應用中,拋出和捕獲的匹配原則存在例外,並不都是絕對相同類型的匹配:利用多態特性拋出派生類對象,使用基類捕獲。
// 1. 異常基類(假設異常類是水果類)
class BaseException {};
// 2. 派生類(蘋果、香蕉,都是水果的子類)
class AppleException : public BaseException {};
class BananaException : public BaseException {};
// 3. 拋出派生類異常(扔蘋果/香蕉)
void doSomething() {
if (/* 某種錯誤 */) throw AppleException(); // 扔蘋果
if (/* 另種錯誤 */) throw BananaException(); // 扔香蕉
}
// 4. 用基類捕獲(只抓“水果”,就接住了蘋果和香蕉)
int main() {
try {
doSomething();
}
// 關鍵:抓基類,就能接住所有派生類異常
catch (const BaseException& e) {
//可以利用多態,不同子類對象產生不同效果
cout << "抓住了異常(不管是蘋果還是香蕉)" << endl;
}
return 0;
}
異常重新拋出
異常重新拋出是在 catch 塊捕獲異常後,不徹底處理,而是再次拋出異常給外層代碼處理的操作。
- 拋原異常:用 throw; (無參數)拋出當前捕獲的異常,保留原始信息(類型、錯誤描述),適用於“內層記錄日誌,外層最終處理”的場景。
void test(){
throw runtime_error("文件讀寫失敗"); // 拋出原始異常
}
void func() {
try {
test();
}
// 內層catch:僅記錄日誌,不徹底處理
catch (const runtime_error& e) {
cout << "[內層] 記錄異常日誌:" << e.what() << endl;
throw; // 關鍵:無參數,拋出原異常給外層
}
}
int main() {
try {
func(); // 觸發異常
}
// 外層catch:最終處理原異常
catch (const runtime_error& e) {
cout << "[外層] 最終處理:" << e.what() << ",已自動重試文件操作" << endl;
}
return 0;
}
- 拋新異常:用 throw 新異常 拋出不同異常(可基於原異常補充信息),適用於“轉換異常類型,適配外層處理邏輯”的場景。
void dbOperate() {
// 內層拋出數據庫專屬異常
throw invalid_argument("數據庫賬號密碼錯誤");
}
int main() {
try
{
//第二個個try可以分函數,也可以寫在第一個try裏面
try
{
dbOperate(); // 觸發數據庫異常
}
// 內層catch:將數據庫異常轉為通用業務異常
catch (const invalid_argument& e)
{
// 拋新異常,補充業務場景説明
throw logic_error("業務初始化失敗:" + string(e.what()));
}
}
// 外層catch:處理通用業務異常
catch (const logic_error& e) {
cout << "[業務層] 處理錯誤:" << e.what() << ",已提示用户檢查配置" << endl;
}
return 0;
}
補充:runtime_error和invalid_argument是C++標準庫提供的標準異常類,屬於std命名空間,定義在頭文件<stdexcpet>
二.異常規範
異常規範是一種聲明函數可能拋出哪些類型異常的語法
異常規範的三種形式
1.動態異常規範(C++98/03)
void func1();//可拋出任意類型異常,默認
void func2()throw(int,double);//拋int,double類型異常
void func3()throw();//不拋異常
void func4()throw(...);//可拋出任意類型異常
目的是讓函數明確聲明“只拋出這幾種異常”,能輕易看出可能出什麼錯誤,且在違反了“承諾”(拋出異常不是聲明的)時,運行崩潰。
但是:這種設計存在問題:①難以維護 ②模板無法聲明具體類型 ③性能開銷以及其他問題無法解決;
所以動態異常規範在C++11年開始標記為廢棄,C++17完全移除(編譯錯誤),C++20徹底消失。
2.noexcept
當動態異常規範被捨棄,C++11引入了新的異常規範(説明符) noexcept;只表明兩種狀態:可能拋異常,不拋異常
void func1()noexcept;//絕對不拋異常
void func2() noexcept(false);//可能拋異常
核心用途
1. 提升性能:編譯器已知函數不拋異常,可省略異常處理相關的冗餘代碼(如棧展開準備),優化執行效率。
2. 明確接口契約:讓調用者清晰知道“此函數無需處理異常”,減少不必要的 try-catch ,簡化代碼。
3. 影響標準庫行為:部分標準庫容器(如 vector)會根據函數是否 noexcept ,選擇更高效的實現(如移動操作)。
3.無規範
void func();//可能拋異常,正常寫就行
三.異常庫
C++ 中與異常相關的“庫”,核心是 <exception> 和 <stdexcept> 兩個頭文件,提供異常處理的基礎類和常用具體異常類型,支撐整個異常機制。(這部分不是很常用,需要查找就可以)
頭文件<exception>
- 功能:定義所有標準異常的基類
std::exception; - 關鍵成員:
virtual const char* what() const noexcept,返回異常描述信息(子類可重寫)。
頭文件 <stdexcept>
- - 功能:定義常用具體異常類,均繼承自
std::exception,覆蓋多數場景。 - - 常見類型(分兩類):
- - 邏輯錯誤(編譯可預判): std::invalid_argument (無效參數)、 std::out_of_range (越界)、 std::logic_error (基類)。
- - 運行時錯誤(運行中出現): std::runtime_error (基類)、 std::overflow_error (算術溢出)、 std::underflow_error (算術下溢)。
其他關聯頭文件
- new:定義 std::bad_alloc (內存分配失敗,如 new 失敗),繼承自 std::exception 。
- typeinfo:定義 std::bad_cast (非法類型轉換,如 dynamic_cast 失敗),繼承自 std::exception 。
四.自定義異常體系
C++ 自定義異常體系,核心是讓自定義異常類繼承標準異常類(如 std::exception 或其子類),既符合標準用法,又能靈活擴展自己的異常信息。
舉個例子:簡單模擬一下服務器開發中通常使用的異常繼承體系
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 = '張三'");
}
//throw "xxxxxx";
}
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;
}
cout << "執行成功" << endl;
//}
return 0;
}
五.異常體系優缺點
優點
- 能夠清晰展現錯誤信息,定位bug
- 不用層層返回
- 更好兼容第三方庫
- 處理越界
缺點
- 執行會直接跳出函數棧,不好調試
- 性能開銷
- C++沒有垃圾回收機制,可能造成內存泄漏,鎖死(RAII)等問題
- 異常規範使用沒有強制要求(早起throw()規範設計僵化,即使用noexcept優化,但需要兼容舊代碼,不能強制設置規範)
六.異常安全
異常安全指代碼在拋出/處理異常時,仍能保證資源不泄露、數據狀態合法、程序可正常恢復的特性,按安全等級從低到高分為3類:
- 基本保證:異常後,數據狀態合法(無破壞),但具體值不確定;
- 強保證:異常後,程序狀態完全回退到異常發生前(要麼操作全成,要麼全不做);
- 無拋保證(noexcept):函數絕對不拋出異常,常見於析構(可能需要釋放內存,拋異常會引發程序終止,導致內存泄漏)、簡單訪問接口。
- 異常處理:是“操作手段”,指用 try/catch/throw 等語法捕獲、拋出、處理異常的具體過程,解決“異常發生時如何響應”的問題。
- 異常安全:是“質量目標”,指代碼在異常處理過程中,仍能保證資源不泄露、數據狀態合法的特性,解決“異常發生後程序如何保持可靠”的問題。