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

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

我們通過(guò)簡(jiǎn)單的程序進(jìn)行驗(yàn)證一下。
double division(int x, int y)
{
if (y == 0)
{
throw "除0錯(cuò)誤";
}
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")這個(gè)打印內(nèi)容并沒(méi)有執(zhí)行,而是直接執(zhí)行catch之后的代碼。
那么現(xiàn)在有一個(gè)問(wèn)題就是,從肉眼角度看,我們的程序直接跳到了catch之后的語(yǔ)句進(jìn)行執(zhí)行,那么func函數(shù)的棧幀還銷毀嗎?func函數(shù)棧幀中申請(qǐng)的那么多臨時(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錯(cuò)誤";
}
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之后的語(yǔ)句進(jìn)行執(zhí)行,但是其實(shí)也是一個(gè)棧幀一個(gè)棧幀的銷毀之后,返回到main函數(shù)的catch語(yǔ)句之后繼續(xù)執(zhí)行,所以這里func函數(shù)的棧幀也是被銷毀的,不會(huì)存在浪費(fèi)空間的問(wèn)題。
在拋出異常之后,如果存在多個(gè)與之相匹配的捕獲代碼,優(yōu)先適用最近原則,誰(shuí)離得近優(yōu)先匹配哪一個(gè)。
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錯(cuò)誤";
}
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(...)可以捕獲任何類型的異常,這個(gè)功能就是防止我們?cè)趻伋霎惓5臅r(shí)候,一不小心拋出一個(gè)沒(méi)有與之相匹配的類型時(shí),導(dǎo)致我們的程序直接終止,就比如像我們實(shí)現(xiàn)一款游戲,為了避免玩家直接進(jìn)行國(guó)粹交談,我們可以捕捉用戶進(jìn)行交流時(shí)的一些不文明用語(yǔ),然后進(jìn)行提示(請(qǐng)規(guī)范語(yǔ)言交流)這樣功能的時(shí)候,如果一旦實(shí)現(xiàn)錯(cuò)誤,我們就要避免我們的程序直接終止,萬(wàn)一這時(shí)候用戶這把游戲特別的關(guān)鍵,由于你實(shí)現(xiàn)一個(gè)功能的失誤,導(dǎo)致程序直接掛掉,這個(gè)代價(jià)就十分的大。所以為了避免這種情況的發(fā)生,如果我們拋出的異常沒(méi)有與之相匹配的話,如果實(shí)現(xiàn)了catch(...),我們的程序還可以正常運(yùn)行,不會(huì)直接終止。
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;
}
異常的拋出和匹配原則
- 異常是通過(guò)拋出對(duì)象而引發(fā)的,這個(gè)拋出對(duì)象的類型是什么,相應(yīng)的就會(huì)激活哪一個(gè)catch的處理代碼。
- 被選中的處理代碼是與拋出對(duì)象類型相匹配且離拋出對(duì)象最近位置的那一個(gè)。
- 拋出異常對(duì)象之后,會(huì)生成一個(gè)異常對(duì)象的拷貝,因?yàn)閽伋龅漠惓?duì)象可能是一個(gè)臨時(shí)對(duì)象,所以會(huì)生成一個(gè)拷貝對(duì)象,這個(gè)拷貝的臨時(shí)對(duì)象會(huì)在被catch以后銷毀。
- catch(...)可以捕獲任何類型的異常,問(wèn)題是不知道異常錯(cuò)誤是什么。
- 實(shí)際中拋出和捕獲的匹配原則有一個(gè)例外,并不是類型完全匹配,可以拋出派生類對(duì)象,使用基類進(jìn)行捕獲。
異常的重新拋出
有可能單個(gè)的catch不能完全處理一個(gè)異常,在進(jìn)行一些校正處理以后,希望再交給更外層的調(diào)用 鏈函數(shù)來(lái)處理,catch則可以通過(guò)重新拋出將異常傳遞給更上層的函數(shù)進(jìn)行處理。
這是什么意思呢?其實(shí)我們通過(guò)一段代碼就可以感受到了。
double division(int x, int y)
{
if (y == 0)
{
throw "除0錯(cuò)誤";
}
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é)果來(lái)看,我們就可以知道當(dāng)正常退出,沒(méi)有異常被拋出的時(shí)候,我們?cè)趂unc函數(shù)棧幀中申請(qǐng)的堆空間資源就可以很好的得到釋放;但是一旦異常被拋出,由于異常獨(dú)特的機(jī)制,這就導(dǎo)致我們釋放堆空間資源的代碼沒(méi)有執(zhí)行,這就會(huì)導(dǎo)致內(nèi)存泄漏,持久下去,一定會(huì)由于內(nèi)存泄漏導(dǎo)致程序掛掉,所以這個(gè)時(shí)候我們就必須將這個(gè)異常重新拋出,將他安全的拋出去,避免內(nèi)存泄漏的問(wèn)題。
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;
}
通過(guò)這樣的方式,在func函數(shù)中對(duì)異常進(jìn)行捕獲,但是不做處理,只是將內(nèi)存安全釋放之后,再將這個(gè)異常拋出去,讓更上層的函數(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;
}
可以看到我們的代碼好像依舊沒(méi)有什么區(qū)別,但是其實(shí)這已經(jīng)有內(nèi)存泄漏的隱患了,這是因?yàn)镃++中用new和delete進(jìn)行內(nèi)存申請(qǐng)和釋放的時(shí)候,一旦申請(qǐng)或者釋放失敗是會(huì)拋出異常的,一旦拋出異常,內(nèi)存沒(méi)有及時(shí)的釋放,就會(huì)導(dǎo)致內(nèi)存泄漏。就比如如下的場(chǎng)景:
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申請(qǐng)內(nèi)存成功之后,arr2申請(qǐng)內(nèi)存時(shí),由于內(nèi)存不夠,導(dǎo)致new拋出了異常,這就會(huì)讓main函數(shù)中的catch對(duì)其進(jìn)行捕獲,這就會(huì)導(dǎo)致arr1申請(qǐng)的內(nèi)存資源沒(méi)有得到釋放,所以這里是不安全的,另外如果我們?cè)诰€程中pthread_mutex_lock 和pthread_mutex_unlock之間如果拋出了異常,也會(huì)導(dǎo)致死鎖,所以我們應(yīng)該如何解決這些問(wèn)題呢?我們?cè)谙乱黄┛偷闹悄苤羔樦泻煤昧私?,這里我們只要了解到異常是會(huì)導(dǎo)致不安全的情況。
還有就是構(gòu)造函數(shù)完成對(duì)象的構(gòu)造和初始化,最好不要在構(gòu)造函數(shù)中拋出異常,否則可能導(dǎo)致對(duì)象不 完整或沒(méi)有完全初始化;同時(shí),析構(gòu)函數(shù)主要完成資源的清理,最好不要在析構(gòu)函數(shù)內(nèi)拋出異常,否則可能導(dǎo)致資源泄漏。
異常規(guī)范
- 異常規(guī)格說(shuō)明的目的是為了讓函數(shù)使用者知道該函數(shù)可能拋出的異常有哪些。 可以在函數(shù)的 后面接throw(類型),列出這個(gè)函數(shù)可能拋擲的所有異常類型。
- 函數(shù)的后面接throw(),表示函數(shù)不拋異常。
- 若無(wú)異常接口聲明,則此函數(shù)可以拋擲任何類型的異常。
但規(guī)范終究還是規(guī)范,只要沒(méi)有強(qiáng)制,規(guī)范還是可以不遵守的,這就好比我們?cè)诮值篱_(kāi)車的時(shí)候,機(jī)動(dòng)車道不能走非機(jī)動(dòng)車道,但還是有人為了趕時(shí)間,看到非機(jī)動(dòng)車道沒(méi)人就開(kāi)進(jìn)行去了,所以異常這里的規(guī)范就比較隨意了。我們可以看看。

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

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

