标题:[C++] 异常详解
@水墨不写bug
目录
正文开始:
一、错误处理方式
在程序运行中,不乏会出现一些错误,这些错误或许在我们的意料之中,也可能在意料之外。有错误就要处理,关于错误处理,不同语言有不同的错误处理方法;简单举几个例子:
C语言
错误处理方式:
- 返回值:C语言通常通过函数的返回值来指示错误。例如,标准I/O库中的
fopen
函数在成功时返回一个指向FILE
对象的指针,在失败时返回NULL
。- 全局变量:C语言还使用全局变量(如
errno
)来记录最近一次系统调用的错误码。通过检查errno
的值,程序可以获取到更多关于错误的信息。- assert断言:可直接终止程序,一般对程序的影响较大。
Java语言
错误处理方式:
- 异常处理:Java采用面向对象的异常处理机制,通过
try-catch-finally-throw
块来捕获和处理异常。Java中的异常分为检查型异常(checked exceptions)和非检查型异常(unchecked exceptions,如RuntimeException
及其子类)。- 资源自动管理:Java 7引入了try-with-resources语句,自动管理实现了
AutoCloseable
接口的资源,如文件、数据库连接等,在try块执行完毕后自动关闭资源。
而C++的错误处理方式虽然与Java类似,都是异常,但是C++与Java仍是有一些区别的。
二、异常的概念
异常是一种处理错误的方式,当一个函数发现自己无法处理的错误时就可以抛出异常,让函数直接或间接的调用者处理这个错误。(准确来说就是通过返回栈帧的方式来返回异常,从而让上一级处理这个异常)
C++的异常处理机制具体是通过下面三个关键字实现的:
throw: 当问题出现时,程序会抛出一个异常。
catch: 设置在想要处理问题的地方,通过异常处理程序捕获异常.catch 关键字用于捕获异常,可以有多个catch进行捕获。
try: try 块中的代码标识将被激活的特定异常,它后面通常跟着一个或多个 catch 块。
如果有一个块抛出一个异常,捕获异常的方法会使用 try 和 catch 关键字。try 块中放置可能抛出异常的代码,try 块中的代码被称为保护代码。catch 块中写某种异常发生后需要做的后续处理,catch 块进入的原则是 抛出的异常的类型与catch 后()内变量的类型完全一致。try { // 保护的标识代码 } catch( ExceptionName e1 ) { // catch 块 } catch( ExceptionName e2 ) { // catch 块 } catch( ExceptionName eN ) { // catch 块 }
三、异常的使用
1.异常的抛出和捕获(基本用法)
抛出异常是通过关键字 “throw” 实现的,假设throw 关键字会抛出一个A类型的对象,则立刻终止当前逻辑,一层一层向上返回栈桢,最终会有两个结果:1.找到匹配的catch ,进入catch;2.没有匹配的catch,终止进程。
异常的抛出和匹配原则
1. 异常是通过抛出对象而引发的,该对象的类型决定了应该激活哪个catch的处理代码。(注意:catch时不会发生隐式类型转换,比如:抛出int,用size_t就无法匹配;抛出const char* ,用string就无法匹配)
实例一:
double Div(double a, double b) { if (b == 0) throw "div by 0";//抛出异常的类型为常量字符串类型 else return a / b; } void func() { double a,b; cin >> a >> b; cout << Div(a, b) << endl; } int main() { try { func(); } catch (const char* my_exception)//通过const char* 类型变量catch,可以匹配 { cout << my_exception << endl; } return 0; }
double Div(double a, double b) { if (b == 0) throw "div by 0";//抛出异常的类型为常量字符串类型 else return a / b; } void func() { double a,b; cin >> a >> b; cout << Div(a, b) << endl; } int main() { try { func(); } catch (string my_exception)//通过string来catch //尽管可以通过string构造函数进行隐式类型转换,但是仍然不能匹配 { cout << my_exception << endl; } return 0; }
2. 被选中的处理代码是调用链中与该对象类型匹配且离抛出异常位置最近的那一个。(如果有多个catch可以与抛出的异常匹配时,只有最先匹配到的catch 会起作用)
3. 抛出异常对象后,会生成一个异常对象的拷贝,因为抛出的异常对象可能是一个临时对象(比如实例一的Div抛出的const char*类型),所以会生成一个拷贝对象,这个拷贝的临时对象会在被catch 匹配以后销毁。(这里的处理类似于函数的传值返回)
4. catch(...)可以捕获任意类型的异常,问题是不知道异常错误是什么。(这也可以用来兜底:在我们自己设计的所有catch块之后,手动添加一个catch(...),这样不至于因为异常没有被捕捉导致进程直接结束)
5. 实际中抛出和捕获的匹配原则有个例外,并不都是类型完全匹配,可以抛出的派生类对、象,使用基类捕获。(在实际项目中十分好用)
在函数调用链中异常栈展开匹配原则
在抛出异常后,会终止当前代码逻辑,然后:
1. 首先检查throw本身是否在try块内部,如果是再查找匹配的catch语句。如果有匹配的,则调到catch的地方进行处理。
2. 没有匹配的catch则退出当前函数栈,继续在调用函数的栈中进行查找匹配的catch。
3. 如果到达main函数的栈,依旧没有匹配的,则终止程序。上述这个沿着调用链查找匹配的catch子句的过程称为栈展开。所以实际中我们最后都要加一个catch(...)捕获任意类型的异常,否则当有异常没捕获,程序就会直接终止。
4. 找到匹配的catch子句并处理以后,会继续沿着catch子句后面继续执行。(特别注意)
2.异常的重新抛出(特殊情况)
有可能单个的catch不能完全处理一个异常,在进行一些校正处理以后,希望再交给更外层的调用链函数来处理,catch则可以通过重新抛出将异常传递给更上层的函数进行处理。
实例二:
double Div(double a, double b)
{
if (b == 0)
throw "div by 0";//抛出异常的类型为常量字符串类型
else
return a / b;
}
void func()
{
//在func中开辟堆区空间,需要手动释放
int* arr = new int[12];
double a,b;
cin >> a >> b;
//调用Div,首先其内部可能会抛出异常
//其次,如果抛出异常,则会直接终止当前逻辑,转而去寻找匹配的catch
//最终,会直接跳转到main函数的catch (const char* my_exception)中
//在一系列操作中国,忽视了delete[] arr, 导致内存泄漏
cout << Div(a, b) << endl;
delete[] arr;
}
int main()
{
try {
func();
}
catch (const char* my_exception)
{
cout << my_exception << endl;
}
return 0;
}
如何改呢?如果没有发生异常,走正常逻辑;如果发生异常,则我们需要在func栈桢层中,捕获异常,然后delete[] arr 后,重新抛出异常,再返回main 处理:
double Div(double a, double b)
{
if (b == 0)
throw "div by 0";//抛出异常的类型为常量字符串类型
else
return a / b;
}
void func()
{
//在func中开辟堆区空间,需要手动释放
int* arr = new int[12];
double a,b;
cin >> a >> b;
try {
cout << Div(a, b) << endl;
}
catch (...)
{
//捕获任意类型异常,在delete arr 之后,再重新抛出任意类型的异常,交给main的逻辑捕获
delete[] arr;
throw;
}
delete[] arr;
}
int main()
{
try {
func();
}
catch (const char* my_exception)
{
cout << my_exception << endl;
}
return 0;
}
3.异常的规范和常见坑点
在《Effective C++》这本书中,强调了一下几点:
1.构造函数完成对象的构造和初始化,不要在构造函数中抛出异常,否则可能导致对象不完整或没有完全初始化。
2.析构函数主要完成资源的清理,不要在析构函数内抛出异常,否则可能导致资源泄漏(内存泄漏、句柄未关闭等)。
C++中异常经常会导致资源泄漏的问题,比如在new和delete中抛出了异常,导致内存泄漏,在lock和unlock之间抛出了异常导致死锁,C++经常使用RAII来解决以上问题。
关键字noexcept
在C++11之前,如C++98,规定在函数头后面加上声明 throw(),throw括号内部写 可能会抛出异常的类型,但是由于这个语法的不规范使用,加上throw可能会导致一些意想不到的错误,所以在C++11,新增了关键字:noexcept;表示保证这个函数不会抛出异常。
其次throw()括号内什么都不写,也表示一样的效果。
thread() noexcept;
thread() throw();
四、标准库的异常体系
C++ 提供了一系列标准的异常,定义在 中,我们可以在程序中使用这些标准的异常。它们是以父子类层次结构组织起来的。
根据规则:
5. 实际中抛出和捕获的匹配原则有个例外,并不都是类型完全匹配,可以抛出的派生类对、象,使用基类捕获。(在实际项目中十分好用)
我们可以得出启示:实际项目组中,我们实际会设计基类异常,在基类的基础上封装自己项目组的异常,这样一来不仅容易区分,也因为 可以抛出的派生类对、象,使用基类捕获,异常就不容易被忽略。
五、 C++异常小结
优点:
1. 异常对象定义好了,相比错误码的方式可以清晰准确的展示出错误的各种信息,甚至可以包含堆栈调用的信息,这样可以帮助更好的定位程序的bug。
缺点:
1. 异常会导致程序的执行流程乱跳,导致非常的混乱,这会导致我们跟踪调试时以及分析程序时,比较困难。
完~
未经作者同意禁止转载