C++異常機(jī)制:棧展開、異常傳播與異常安全
C語言中的處理錯誤的方式
傳統(tǒng)的錯誤處理機(jī)制:
- 終止程序,如assert,發(fā)生內(nèi)存錯誤,除零錯誤的時候就會終止程序。
#include <assert.h>
int main() {
int *p = NULL;
assert(p != NULL); // 條件不成立,程序直接終止
}- 返回錯誤碼(這是 C 語言中最主流、最標(biāo)準(zhǔn)的錯誤處理方式)
函數(shù)通過返回值告訴調(diào)用者是否出錯:成功 → 返回正常值(如 0);失敗 → 返回錯誤碼(如 -1)
int func() {
if (發(fā)生錯誤)
return -1;
return 0;
}- errno + 錯誤信息(系統(tǒng)級錯誤)
在系統(tǒng)調(diào)用或標(biāo)準(zhǔn)庫函數(shù)中,常用 errno 來記錄錯誤原因。
errno:全局變量strerror(errno):將錯誤碼轉(zhuǎn)成可讀字符串perror():直接打印錯誤信息
#include <stdio.h>
#include <errno.h>
#include <string.h>
int main() {
FILE *fp = fopen("test.txt", "r");
if (fp == NULL) {
printf("error: %s\n", strerror(errno));
perror("fopen failed");
}
}
C++異常的概念
異常是一種處理錯誤的方式,當(dāng)一個函數(shù)發(fā)現(xiàn)存在錯誤時就會拋出異常,讓函數(shù)的直接或者間接調(diào)用者去處理這個錯誤,而不是直接終止這個程序。
- throw:當(dāng)問題出現(xiàn)的時候,程序就會拋出一個異常,這個時候就是通過throw這個關(guān)鍵字來完成的。
- catch:catch關(guān)鍵字用來捕獲異常,可以有多個catch進(jìn)行捕獲。
- try:try塊中的代碼標(biāo)識將激活特定的異常,他后面通常跟著一個或多個catch塊。
如果有一個塊拋出一個異常,捕獲異常的方法會使用 try 和 catch 關(guān)鍵字。try 塊中放置可能拋 出異常的代碼,try 塊中的代碼被稱為保護(hù)代碼。
異常的使用
如果我們只拋出了異常,并沒有進(jìn)行捕獲,一旦程序檢查到錯誤,就會直接報(bào)錯終止程序。
double division(int x, int y)
{
if (y == 0)
{
throw "除0錯誤";
}
else
{
return (double)x / (double)y;
}
}
void func()
{
int x, y;
std::cin >> x >> y;
std::cout << division(x, y) << std::endl;
}
int main()
{
func();
return 0;
}
所以當(dāng)我們拋出異常的時候,一定要進(jìn)行捕獲。
我們在進(jìn)行捕獲異常的時候,拋出的對象是什么類型的,相應(yīng)的我們進(jìn)行捕獲時的類型也要相匹配。
double division(int x, int y)
{
if (y == 0)
{
throw "除0錯誤";
}
else
{
return (double)x / (double)y;
}
}
void func()
{
int x, y;
std::cin >> x >> y;
std::cout << division(x, y) << std::endl;
}
int main()
{
try
{
func();
}
catch (const char* s)
{
std::cout << s << std::endl;
}
return 0;
}
如果捕獲時沒有與拋出時相匹配的類型,程序就會報(bào)錯,直接終止程序。

找到匹配的catch子句并處理以后,會繼續(xù)沿著catch子句后面繼續(xù)執(zhí)行。就比如這段程序,在正常情況下應(yīng)該時main函數(shù)->func函數(shù)->division函數(shù),最后通過division函數(shù)->func函數(shù)->main函數(shù),這樣的方式進(jìn)行返回,但是如果時拋出異常之后,程序就會執(zhí)行main函數(shù)catch之后的代碼。

我們通過簡單的程序進(jìn)行驗(yàn)證一下。
double division(int x, int y)
{
if (y == 0)
{
throw "除0錯誤";
}
else
{
return (double)x / (double)y;
}
}
void func()
{
int x, y;
std::cin >> x >> y;
std::cout << division(x, y) << std::endl;
std::cout << "xxxxxxxxxxxxxxxxx" << std::endl;
}
int main()
{
try
{
func();
}
catch (const char* s)
{
std::cout << s << std::endl;
}
std::cout << "yyyyyyyyyyyyyyyyy" << std::endl;
return 0;
}
可以看到func函數(shù)中的("xxxxxxxxxxxxxxxxx")這個打印內(nèi)容并沒有執(zhí)行,而是直接執(zhí)行catch之后的代碼。
那么現(xiàn)在有一個問題就是,從肉眼角度看,我們的程序直接跳到了catch之后的語句進(jìn)行執(zhí)行,那么func函數(shù)的棧幀還銷毀嗎?func函數(shù)棧幀中申請的那么多臨時變量所占的空間要是不銷毀,那不是浪費(fèi)空間嗎?
class A
{
public:
A()
{
std::cout << "A()" << std::endl;
}
~A()
{
std::cout << "~A()" << std::endl;
}
};
double division(int x, int y)
{
if (y == 0)
{
throw "除0錯誤";
}
else
{
return (double)x / (double)y;
}
}
void func()
{
A a;
int x, y;
std::cin >> x >> y;
std::cout << division(x, y) << std::endl;
std::cout << "xxxxxxxxxxxxxxxxx" << std::endl;
}
int main()
{
try
{
func();
}
catch (const char* s)
{
std::cout << s << std::endl;
}
std::cout << "yyyyyyyyyyyyyyyyy" << std::endl;
return 0;
}
可以看到,即使我們從直觀角度看,好像是直接跳轉(zhuǎn)到了catch之后的語句進(jìn)行執(zhí)行,但是其實(shí)也是一個棧幀一個棧幀的銷毀之后,返回到main函數(shù)的catch語句之后繼續(xù)執(zhí)行,所以這里func函數(shù)的棧幀也是被銷毀的,不會存在浪費(fèi)空間的問題。
在拋出異常之后,如果存在多個與之相匹配的捕獲代碼,優(yōu)先適用最近原則,誰離得近優(yōu)先匹配哪一個。
class A
{
public:
A()
{
std::cout << "A()" << std::endl;
}
~A()
{
std::cout << "~A()" << std::endl;
}
};
double division(int x, int y)
{
if (y == 0)
{
throw "除0錯誤";
}
else
{
return (double)x / (double)y;
}
}
void func()
{
A a;
int x, y;
std::cin >> x >> y;
try
{
std::cout << division(x, y) << std::endl;
}
catch (const char* s)
{
std::cout << s << std::endl;
}
std::cout << "xxxxxxxxxxxxxxxxx" << std::endl;
}
int main()
{
try
{
func();
}
catch (const char* s)
{
std::cout << s << std::endl;
}
std::cout << "yyyyyyyyyyyyyyyyy" << std::endl;
return 0;
}
異常中還有一點(diǎn)就是catch(...)可以捕獲任何類型的異常,這個功能就是防止我們在拋出異常的時候,一不小心拋出一個沒有與之相匹配的類型時,導(dǎo)致我們的程序直接終止,就比如像我們實(shí)現(xiàn)一款游戲,為了避免玩家直接進(jìn)行國粹交談,我們可以捕捉用戶進(jìn)行交流時的一些不文明用語,然后進(jìn)行提示(請規(guī)范語言交流)這樣功能的時候,如果一旦實(shí)現(xiàn)錯誤,我們就要避免我們的程序直接終止,萬一這時候用戶這把游戲特別的關(guān)鍵,由于你實(shí)現(xiàn)一個功能的失誤,導(dǎo)致程序直接掛掉,這個代價就十分的大。所以為了避免這種情況的發(fā)生,如果我們拋出的異常沒有與之相匹配的話,如果實(shí)現(xiàn)了catch(...),我們的程序還可以正常運(yùn)行,不會直接終止。
class A
{
public:
A()
{
std::cout << "A()" << std::endl;
}
~A()
{
std::cout << "~A()" << std::endl;
}
};
double division(int x, int y)
{
if (y == 0)
{
throw 33;
}
else
{
return (double)x / (double)y;
}
}
void func()
{
A a;
int x, y;
std::cin >> x >> y;
try
{
std::cout << division(x, y) << std::endl;
}
catch (const char* s)
{
std::cout << s << std::endl;
}
std::cout << "xxxxxxxxxxxxxxxxx" << std::endl;
}
int main()
{
try
{
func();
}
catch (const char* s)
{
std::cout << s << std::endl;
}
catch (...)
{
std::cout << "未知異常" << std::endl;
}
std::cout << "yyyyyyyyyyyyyyyyy" << std::endl;
return 0;
}
異常的拋出和匹配原則
- 異常是通過拋出對象而引發(fā)的,這個拋出對象的類型是什么,相應(yīng)的就會激活哪一個catch的處理代碼。
- 被選中的處理代碼是與拋出對象類型相匹配且離拋出對象最近位置的那一個。
- 拋出異常對象之后,會生成一個異常對象的拷貝,因?yàn)閽伋龅漠惓ο罂赡苁且粋€臨時對象,所以會生成一個拷貝對象,這個拷貝的臨時對象會在被catch以后銷毀。
- catch(...)可以捕獲任何類型的異常,問題是不知道異常錯誤是什么。
- 實(shí)際中拋出和捕獲的匹配原則有一個例外,并不是類型完全匹配,可以拋出派生類對象,使用基類進(jìn)行捕獲。
異常的重新拋出
有可能單個的catch不能完全處理一個異常,在進(jìn)行一些校正處理以后,希望再交給更外層的調(diào)用 鏈函數(shù)來處理,catch則可以通過重新拋出將異常傳遞給更上層的函數(shù)進(jìn)行處理。
這是什么意思呢?其實(shí)我們通過一段代碼就可以感受到了。
double division(int x, int y)
{
if (y == 0)
{
throw "除0錯誤";
}
else
{
return (double)x / (double)y;
}
}
void func()
{
int* arr = new int[10];
int x, y;
std::cin >> x >> y;
std::cout << division(x, y) << std::endl;
std::cout << "delete[] : " << arr << std::endl;
delete[] arr;
}
int main()
{
try
{
func();
}
catch (const char* s)
{
std::cout << s << std::endl;
}
catch (...)
{
std::cout << "未知異常" << std::endl;
}
std::cout << "yyyyyyyyyyyyyyyyy" << std::endl;
return 0;
}

從結(jié)果來看,我們就可以知道當(dāng)正常退出,沒有異常被拋出的時候,我們在func函數(shù)棧幀中申請的堆空間資源就可以很好的得到釋放;但是一旦異常被拋出,由于異常獨(dú)特的機(jī)制,這就導(dǎo)致我們釋放堆空間資源的代碼沒有執(zhí)行,這就會導(dǎo)致內(nèi)存泄漏,持久下去,一定會由于內(nèi)存泄漏導(dǎo)致程序掛掉,所以這個時候我們就必須將這個異常重新拋出,將他安全的拋出去,避免內(nèi)存泄漏的問題。
void func()
{
int* arr = new int[10];
try
{
int x, y;
std::cin >> x >> y;
std::cout << division(x, y) << std::endl;
}
catch (...)
{
std::cout << "delete[] : " << arr << std::endl;
delete[] arr;
throw;
}
std::cout << "delete[] : " << arr << std::endl;
delete[] arr;
}
通過這樣的方式,在func函數(shù)中對異常進(jìn)行捕獲,但是不做處理,只是將內(nèi)存安全釋放之后,再將這個異常拋出去,讓更上層的函數(shù)進(jìn)行處理。
異常安全
我們將上面的代碼中的func進(jìn)行更改一下。
void func()
{
int* arr1 = new int[10];
int* arr2 = new int[20];
try
{
int x, y;
std::cin >> x >> y;
std::cout << division(x, y) << std::endl;
}
catch (...)
{
std::cout << "delete[] : " << arr1 << std::endl;
delete[] arr1;
std::cout << "delete[] : " << arr2 << std::endl;
delete[] arr2;
throw;
}
std::cout << "delete[] : " << arr1 << std::endl;
delete[] arr1;
std::cout << "delete[] : " << arr2 << std::endl;
delete[] arr2;
}
可以看到我們的代碼好像依舊沒有什么區(qū)別,但是其實(shí)這已經(jīng)有內(nèi)存泄漏的隱患了,這是因?yàn)镃++中用new和delete進(jìn)行內(nèi)存申請和釋放的時候,一旦申請或者釋放失敗是會拋出異常的,一旦拋出異常,內(nèi)存沒有及時的釋放,就會導(dǎo)致內(nèi)存泄漏。就比如如下的場景:
void func()
{
int* arr1 = new int[10];
int* arr2 = new int[536870911];
try
{
int x, y;
std::cin >> x >> y;
std::cout << division(x, y) << std::endl;
}
catch (...)
{
std::cout << "delete[] : " << arr1 << std::endl;
delete[] arr1;
std::cout << "delete[] : " << arr2 << std::endl;
delete[] arr2;
throw;
}
std::cout << "delete[] : " << arr1 << std::endl;
delete[] arr1;
std::cout << "delete[] : " << arr2 << std::endl;
delete[] arr2;
}
當(dāng)我們的arr1申請內(nèi)存成功之后,arr2申請內(nèi)存時,由于內(nèi)存不夠,導(dǎo)致new拋出了異常,這就會讓main函數(shù)中的catch對其進(jìn)行捕獲,這就會導(dǎo)致arr1申請的內(nèi)存資源沒有得到釋放,所以這里是不安全的,另外如果我們在線程中pthread_mutex_lock 和pthread_mutex_unlock之間如果拋出了異常,也會導(dǎo)致死鎖,所以我們應(yīng)該如何解決這些問題呢?我們在下一篇博客的智能指針中好好了解,這里我們只要了解到異常是會導(dǎo)致不安全的情況。
還有就是構(gòu)造函數(shù)完成對象的構(gòu)造和初始化,最好不要在構(gòu)造函數(shù)中拋出異常,否則可能導(dǎo)致對象不 完整或沒有完全初始化;同時,析構(gòu)函數(shù)主要完成資源的清理,最好不要在析構(gòu)函數(shù)內(nèi)拋出異常,否則可能導(dǎo)致資源泄漏。
異常規(guī)范
- 異常規(guī)格說明的目的是為了讓函數(shù)使用者知道該函數(shù)可能拋出的異常有哪些。 可以在函數(shù)的 后面接throw(類型),列出這個函數(shù)可能拋擲的所有異常類型。
- 函數(shù)的后面接throw(),表示函數(shù)不拋異常。
- 若無異常接口聲明,則此函數(shù)可以拋擲任何類型的異常。
但規(guī)范終究還是規(guī)范,只要沒有強(qiáng)制,規(guī)范還是可以不遵守的,這就好比我們在街道開車的時候,機(jī)動車道不能走非機(jī)動車道,但還是有人為了趕時間,看到非機(jī)動車道沒人就開進(jìn)行去了,所以異常這里的規(guī)范就比較隨意了。我們可以看看。

可以看到,我明明只寫了可能會拋出int類型的異常,但是const char*類型的異常它依舊可以接收。

throw()表示這個函數(shù)不拋出異常,但是const char*類型的異常它依舊可以接收。
所以規(guī)范也僅僅只是規(guī)范,還是可以不遵守的。
自定義異常體系
現(xiàn)在假如現(xiàn)在有10個人要合理開發(fā)一個程序,讓每一個去實(shí)現(xiàn)一部分功能,核心成員完成整體的框架,但是每一個在實(shí)現(xiàn)功能的時候,都可能會拋出異常,這讓最外面負(fù)責(zé)接收異常的人就非常的痛苦了,因?yàn)槊慷鄴伋鲆粋€異常,他就要增加一個對應(yīng)的catch代碼進(jìn)行捕捉,十分的頭疼。
為了解決這一問題,可以構(gòu)建一個統(tǒng)一的異常體系:定義一個異?;?Exception,讓各個模塊在拋出異常時都繼承該基類并拋出其派生類對象。這樣,在頂層只需通過 catch(const Exception& e) 即可捕獲所有類型的異常,并借助虛函數(shù)實(shí)現(xiàn)多態(tài),根據(jù)實(shí)際對象類型調(diào)用對應(yīng)的 what() 方法獲取具體錯誤信息。這種設(shè)計(jì)不僅降低了模塊之間的耦合,還提升了代碼的擴(kuò)展性與可維護(hù)性,當(dāng)系統(tǒng)新增異常類型時,無需修改原有的捕獲邏輯。
class Exception
{
public:
Exception(const std::string& errmsg, int id)
:_errmsg(errmsg)
, _id(id)
{}
virtual std::string what() const
{
return _errmsg;
}
protected:
std::string _errmsg;
int _id;
};
class SqlException : public Exception
{
public:
SqlException(const std::string& errmsg, int id, const std::string& sql)
:Exception(errmsg, id)
, _sql(sql)
{}
virtual std::string what() const
{
std::string str = "SqlException:";
str += _errmsg;
str += "->";
str += _sql;
return str;
}
private:
const std::string _sql;
};
class HttpServerException : public Exception
{
public:
HttpServerException(const std::string& errmsg, int id, const std::string& type)
:Exception(errmsg, id)
, _type(type)
{}
virtual std::string what() const
{
std::string str = "HttpServerException:";
str += _type;
str += ":";
str += _errmsg; return str;
}
private:
const std::string _type;
};
void SQLMgr()
{
srand(time(0));
if (rand() % 7 == 0)
{
throw SqlException("權(quán)限不足", 100, "select * from name = '張三'");
}
//throw "xxxxxx";
}
void HttpServer()
{
// ...
srand(time(0));
if (rand() % 3 == 0)
{
throw HttpServerException("請求資源不存在", 100, "get");
}
else if (rand() % 4 == 0)
{
throw HttpServerException("權(quán)限不足", 101, "post");
}
SQLMgr();
}
int main()
{
while (1)
{
Sleep(500);
try {
HttpServer();
}
catch (const Exception& e) // 這里捕獲父類對象就可以
{
// 多態(tài)
std::cout << e.what() << std::endl;
}
catch (...)
{
std::cout << "Unkown Exception" << std::endl;
}
}
return 0;
}
C++異常的本質(zhì)是為了改變程序處理錯誤的方式。它讓錯誤不再被埋在一層層返回值里,而是被明確地拋出來、傳遞出去,直到被真正處理。但異常也不是“萬能解藥”。
用得好,它是解耦利器;用不好,它就是一顆隨時引爆的雷——資源泄漏、邏輯混亂、程序失控,往往都從這里開始。
所以關(guān)鍵不在于“會不會用異常”,而在于:你是否真的理解——
異常發(fā)生時,程序到底在做什么?資源有沒有被正確管理?錯誤有沒有被合理接???
到此這篇關(guān)于C++異常機(jī)制:棧展開、異常傳播與異常安全的文章就介紹到這了,更多相關(guān)C++異常處理內(nèi)容請搜索腳本之家以前的文章或繼續(xù)瀏覽下面的相關(guān)文章希望大家以后多多支持腳本之家!
相關(guān)文章
C++中的std::funture和std::promise實(shí)例詳解
在線程池中獲取線程執(zhí)行函數(shù)的返回值時,通常使用 std::future 而不是 std::promise 來傳遞返回值,這篇文章主要介紹了C++中的std::funture和std::promise實(shí)例詳解,需要的朋友可以參考下2024-05-05
C++ Qt開發(fā)之使用QTcpSocket實(shí)現(xiàn)TCP網(wǎng)絡(luò)通信
Qt 是一個跨平臺C++圖形界面開發(fā)庫,利用Qt可以快速開發(fā)跨平臺窗體應(yīng)用程序,本文主要為大家介紹了如何運(yùn)用QTcpSocket組件實(shí)現(xiàn)基于TCP的網(wǎng)絡(luò)通信功能,需要的可以參考下2024-03-03
C/C++的堆棧內(nèi)存分配的實(shí)現(xiàn)
內(nèi)存管理是至關(guān)重要的一個方面,堆和棧是C語言中重要的內(nèi)存分配方式,本文主要介紹了C/C++的堆棧內(nèi)存分配的實(shí)現(xiàn),詳細(xì)的介紹了這兩者在管理方式、性能和使用場景,感興趣的可以了解一下2024-07-07
Recommended C Style and Coding Standards中文翻譯版
本文翻譯自Recommended C Style and Coding Standards(C語言編碼風(fēng)格和標(biāo)準(zhǔn)),需要的朋友可以參考下2014-04-04
解析Linux下的時間函數(shù):設(shè)置以及獲取時間的方法
本篇文章是對Linux下的時間函數(shù):設(shè)置以及獲取時間的方法進(jìn)行了詳細(xì)的分析介紹,需要的朋友參考下2013-05-05

