概述
异常是指程序在执行的过程中,没有按照预定的流程和逻辑去运行,从而导致数组越界、内存溢出、甚至程序崩溃等各种非正常的情况。在C++、Java和C#等高级语言中,都提供了对于异常的处理机制。异常处理,实际上是一种转移程序控制权的方式。当程序中抛出了异常时,我们可以捕获异常,进而进行相应的处理。处理模型一般有两种:一种是终止模型,表示该异常是致命的,无法恢复,会直接终止程序;另一种是恢复模型,表示该异常是临时的,可恢复的,会尝试去修正错误,确保程序可以继续运行。
C语言中的异常处理
在介绍C++中的异常处理机制之前,我们先聊一聊C语言中的异常处理机制,这样便于我们更好地理解相关概念。在C语言中,一般有如下几种异常处理方式。
1、使用断言assert宏。当断言不通过时,会终止程序的执行。注意:assert仅在调试版本(Debug)下有效,在发布版本(Release)下无效。这就意味着,千万不要在assert中编写正常业务流程中会执行的业务代码,因为在发布版本(Release)下,这些业务代码根本不会执行。
void SetText(const char *pszText)
{
assert(pszText != NULL);
}
SetText(NULL);
在Debug模式下运行上面的代码后,会直接弹出下面的警告框,提示我们触发了断言,程序已终止。
继续来看下面的代码。
static const char *g_pszText = NULL;
void SetText(const char *pszText)
{
assert((g_pszText = pszText) != NULL);
if (g_pszText == NULL)
{
printf("text is null\n");
}
else
{
printf("text is %s\n", g_pszText);
}
}
SetText("CSDN");
在Debug下运行该程序时,输出为:text is CSDN。但在Release下运行该程序时,输出为:text is null。这是因为,Release下assert断言中的表达式不会被执行,g_pszText没有被赋值,仍是原来的NULL。
2、使用exit和atexit函数终止程序执行,并捕获异常信息。
int UpdateNumber(int nNumber)
{
if (nNumber == 0)
{
exit(-1);
}
// ...
return 0;
}
void ExitFunc()
{
printf("app exited\n");
}
atexit(ExitFunc);
UpdateNumber(0);
执行上面的程序后,会输出:app exited,然后程序终止运行。
3、使用errno这个全局错误码。errno既可以由代码写入,也可以由代码读取。
int UpdateNumber(int nNumber)
{
if (nNumber == 0)
{
errno = 66;
return -1;
}
return 0;
}
int nRet = UpdateNumber(0);
if (nRet != 0)
{
printf("update error: %d, %d\n", nRet, errno);
}
执行上面的程序后,会输出:update error: -1, 66。
4、使用goto。当发生异常时,进行程序逻辑的跳转,只能在一个函数内进行局部跳转。
int UpdateNumber(int nNumber)
{
if (nNumber == 0)
{
goto WRONG;
}
return 0;
WRONG:
return -1;
}
int nRet = UpdateNumber(0);
if (nRet != 0)
{
printf("update error: %d\n", nRet);
}
执行上面的程序后,会输出:update error: -1。
5、使用setjmp和longjmp。这两个函数结合使用时,可以进行非局部跳转。
int UpdateNumber(int nNumber)
{
if (nNumber == 0)
{
goto WRONG;
}
return 0;
WRONG:
return -1;
}
int nRet = UpdateNumber(0);
if (nRet != 0)
{
printf("update error: %d\n", nRet);
}
执行上面的程序后,会输出:update error: -1。
再来看看下面的代码。
static jmp_buf s_jmpBuf;
class CBase
{
public:
CBase()
{
printf("CBase constructor\n");
}
~CBase()
{
printf("CBase destructor\n");
}
};
int UpdateNumber(int nNumber)
{
if (nNumber == 0)
{
CBase base;
longjmp(s_jmpBuf, -1);
}
return 0;
}
int nRet = setjmp(s_jmpBuf);
if (nRet == 0)
{
UpdateNumber(0);
}
else
{
printf("update error: %d\n", nRet);
}
在有的编译器下,执行程序后的输出如下。
CBase constructor
CBase destructor
update error: -1
但在有的编译器下,执行程序后的输出如下。
CBase constructor
update error: -1
此时,base对象只进入了构造函数,却没有进入析构函数。实际上,对于longjmp跳转前局部对象是否需要析构这一点,C++标准并无明确要求,这取决于编译器的具体实现。
可以看到,断言assert宏、exit和atexit函数都是直接终止程序的运行,没有提供异常恢复的能力;errno实际上与判断函数返回值没有本质区别,当在多个地方调用同一个函数时,仍需要在每个地方对函数返回值和errno进行判断,会造成代码重复和臃肿;goto会造成逻辑跳转的混乱,且只能进行局部跳转;setjmp和longjmp虽然可以进行非局部跳转,但longjmp后,之前声明的对象实例可能不会调用析构函数释放内存。
正是由于这些原因,C++中引入了新的异常处理机制。
使用要点
1、在C++中,使用try、throw和catch三个关键字来处理异常。try用于包含可能会抛出异常的代码段,throw用于抛出某个异常,catch用于捕获异常。
int UpdateNumber(int nNumber)
{
if (nNumber == 0)
{
throw "number is zero\n";
}
return 0;
}
try
{
UpdateNumber(0);
}
catch (const char *pszError)
{
printf(pszError);
}
2、在try中声明的变量,外部访问不了,catch中也访问不了。
try
{
int nData = 66;
UpdateNumber(0);
}
catch (const char *pszError)
{
nData == 88; // 编译错误,访问不了try中声明的变量
printf(pszError);
}
nData == 99; // 编译错误,访问不了try中声明的变量
3、使用throw抛出异常后,throw后面到本函数结束之间的所有代码都不会执行。
int UpdateNumber(int nNumber)
{
if (nNumber == 0)
{
throw "number is zero\n";
printf("update throw\n");
}
printf("update ok\n");
return 0;
}
try
{
UpdateNumber(0);
}
catch (const char *pszError)
{
printf(pszError);
}
执行上面的代码后,既不会输出:update throw,也不会输出:update ok。
4、catch可以有多个,但至少要有一个。有多个catch时,具体进入哪个catch,要看throw抛出的异常类型与哪个catch的异常类型最匹配。
int UpdateNumber(int nNumber)
{
if (nNumber == 0)
{
throw "number is zero\n";
}
return 0;
}
try
{
UpdateNumber(0);
}
catch (int nError)
{
printf("error is %d\n", nError);
}
catch (const char *pszError)
{
printf(pszError);
}
执行上面的代码后,输出:number is zero。虽然int类型的catch写在了前面,但与throw抛出的字符串类型并不匹配,故最终还是进入了字符串类型的catch。
5、想捕获所有异常,可以直接使用catch(...)。
try
{
UpdateNumber(0);
}
catch (...)
{
printf("exception caught\n");
}
6、catch进行类型匹配时,不会发生隐式类型转换。
int UpdateNumber(int nNumber)
{
if (nNumber == 0)
{
short sTemp = 66;
throw sTemp;
}
return 0;
}
try
{
UpdateNumber(0);
}
catch (int nError)
{
printf("error is int\n");
}
catch (short sError)
{
printf("error is short\n");
}
执行上面的代码后,输出:error is short。这是因为,throw抛出的数据类型short在与catch匹配时,并不会隐式类型转换为int。
7、throw中抛出的异常类型是对象时,可以是对象的值,也可以是指向对象的指针。对应的,catch中匹配的,可以是对象的引用,也可以是指向对象的指针。
class CBase
{
public:
const char *What()
{
return "base exception";
}
};
int UpdateNumber(int nNumber)
{
if (nNumber == 0)
{
throw CBase();
}
if (nNumber == 1)
{
throw new CBase();
}
return 0;
}
try
{
UpdateNumber(0);
}
catch (CBase &base)
{
// 捕获对象的引用
printf("error is base: %s\n", base.What());
}
try
{
UpdateNumber(1);
}
catch (CBase *pBase)
{
// 捕获指向对象的指针
printf("error is pBase: %s\n", pBase->What());
delete pBase;
}
8、throw抛出的异常类型是派生类对象时,catch可以用基类对象捕获。
class CBase
{
public:
const char *What()
{
return "base exception";
}
};
class CDerived : public CBase
{
};
int UpdateNumber(int nNumber)
{
if (nNumber == 0)
{
throw CDerived();
}
if (nNumber == 1)
{
throw new CDerived();
}
return 0;
}
try
{
UpdateNumber(0);
}
catch (CBase &base)
{
// 可正常捕获派生类的异常
printf("error is base: %s\n", base.What());
}
try
{
UpdateNumber(1);
}
catch (CBase *pBase)
{
// 可正常捕获派生类的异常
printf("error is pBase: %s\n", pBase->What());
delete pBase;
}
9、throw抛出异常后,可以在catch中进行处理并继续抛出异常。
int UpdateNumber(int nNumber)
{
if (nNumber == 0)
{
throw 66;
}
return 0;
}
void Test()
{
try
{
UpdateNumber(0);
}
catch (int nError)
{
printf("test error is: %d\n", nError);
throw; // 继续抛出异常
}
}
try
{
Test();
}
catch (int nError)
{
printf("main error is: %d\n", nError);
}
执行上面的代码后,输出如下。
test error is: 66
main error is: 66
10、抛出异常后,try和throw之间所有的局部对象都会自动调用析构函数。
class CBase
{
public:
CBase(int nData) : m_nData(nData)
{
printf("CBase constructor: %d\n", nData);
}
~CBase()
{
printf("CBase destructor: %d\n", m_nData);
}
private:
int m_nData;
};
int UpdateNumber(int nNumber)
{
if (nNumber == 0)
{
CBase base(88);
throw 1;
}
return 0;
}
try
{
CBase base(66);
UpdateNumber(0);
}
catch (int nError)
{
printf("error is: %d\n", nError);
}
执行上面的代码后,输出如下。
CBase constructor: 66
CBase constructor: 88
CBase destructor: 88
CBase destructor: 66
error is: 1
可以看到,throw之前声明的局部变量均被正常析构了。
11、异常被抛出后,会沿着调用堆栈一直查找匹配的catch。如果直到main函数都没有被catch,则会导致程序自动调用terminate函数而终止运行。
12、可以在声明和定义函数时,为函数添加异常声明列表,用于指明该函数可能会抛出哪些异常,以增强程序的可读性。异常声明列表可以在声明函数时添加,也可以在定义函数时添加,如果两处都添加,则需要保证一致。如果一个函数不抛出任何异常,则可以用throw()来表示。如果不添加异常声明列表,则表示可以抛出任何异常。
// 可抛出int和const char *类型的异常
int UpdateNumber(int nNumber) throw(int, const char *)
{
if (nNumber == 0)
{
throw 66;
}
return 0;
}
// 不抛出异常
void Test() throw()
{
printf("test func\n");
}
// 可抛出任何异常
void Test2()
{
printf("test2 func\n");
}
在C++ 11中,取消了对异常声明列表的支持,并添加了noexcept,用于表明函数是否会抛出异常。
// 会抛出异常
int UpdateNumber(int nNumber) noexcept(false)
{
if (nNumber == 0)
{
throw 66;
}
return 0;
}
// 不会抛出异常
void Test() noexcept(true)
{
printf("test func\n");
}
13、C++中已经定义了很多标准异常,这些标准异常都是从std::exception类派生的,具体可以参考下面的表格。
异常基类
异常派生类
描述
std::bad_alloc
--
该异常可以通过new抛出
std::bad_cast
--
该异常可以通过dynamic_cast抛出
std::bad_typeid
--
该异常可以通过typeid抛出
std::bad_exception
--
表示无法预期的异常
std::logic_error
--
理论上可以通过读取代码来检测到的异常
--
std::domain_error
当使用了一个无效的数学域时,会抛出该异常
--
std::invalid_argument
当使用了无效的参数时,会抛出该异常
--
std::length_error
当创建了太长的std::string时,会抛出该异常
--
std::out_of_range
当访问std::vector的元素越界时,会抛出该异常
std::runtime_error
--
理论上不可以通过读取代码来检测到的异常
--
std::overflow_error
当发生数学上溢时,会抛出该异常
--
std::range_error
当尝试存储超出范围的值时,会抛出该异常
--
std::underflow_error
当发生数学下溢时,会抛出该异常
下面的代码列出了一些会抛出标准异常的情形。
try
{
char *pData = new char[0x7FFFFFFF];
}
catch (std::bad_alloc &e)
{
printf("%s\n", e.what());
}
try
{
std::vector<int> vctNumber(5);
vctNumber.push_back(66);
int nNumber = vctNumber.at(9);
}
catch (std::out_of_range &e)
{
printf("%s\n", e.what());
}
执行上面的代码后,输出如下。
bad allocation
invalid vector subscript
14、除了使用标准异常,我们也可以自定义异常处理类。
class CMyException : public std::exception
{
public:
CMyException(int nError, const char *pszError) : std::exception(pszError), m_nError(nError)
{
NULL;
}
int GetErrorCode() const
{
return m_nError;
}
private:
int m_nError;
};
int UpdateNumber(int nNumber)
{
if (nNumber == 0)
{
throw CMyException(66, "number is zero");
}
return 0;
}
try
{
UpdateNumber(0);
}
catch (CMyException &me)
{
printf("error is: %d, %s\n", me.GetErrorCode(), me.what());
}
执行上面的代码后,输出为:error is: 66, number is zero。
15、尽量不要在构造函数中抛出异常,因为此时不会调用析构函数,可能会导致内存泄漏和资源未释放。
class CBase
{
public:
CBase() : m_pData(NULL)
{
printf("base constructor\n");
m_pData = new char[66];
throw 1;
}
~CBase()
{
delete[] m_pData;
m_pData = NULL;
printf("base destructor\n");
}
private:
char *m_pData;
};
try
{
CBase base;
}
catch (...)
{
printf("exception occured\n");
}
上面的代码执行后,输出如下。
base constructor
exception occured
可以看到,base对象没有调用析构函数,从而导致构造函数中申请的内存m_pData没有被释放。
版权归原作者 hope_wisdom 所有, 如有侵权,请联系我们删除。