8-异常与错误
- 1、简介
- 2、异常处理
- 2.1 抛出异常
- 2.2 捕获异常
- 2.3 匹配顺序
- 3、异常说明
- 4、构造函数中的异常
- 5、析构函数中的异常
- 6、标准库异常
1、简介
在程序编码过程中难免会出现错误,主要有:语法错误、逻辑错误、功能错误等,当我们面对以上错误时处理主要针对在实际运行环境中发生,却在设
计、编码和测试阶段无法预料的,当面对异常时,有三种典型的处理机制:
- 通过返回值返回错误信息
- 所有局部对象都能正确地被析构
- 逐层判断,流程繁琐
- 借助setjmp,/longjmp远程跳转(不建议使用,这种效率最高,但是如果使用不当,会造成更加严重的后果)
- 一步到位,流程简单
- 某些局部对象可能因此丧失被析构的机会
- 抛出—捕获异常对象(c++推荐使用)
- 形式上一步到位,流程简单
- 实际上逐层析构局部对象,避免内存泄漏
2、异常处理
2.1 抛出异常
语法:throw 异常对象
- 可以抛出基本类型的对象,如:
void foo(){
FILE * txt = fopen("./a.txt", "r");
if (!txt){
cout << "文件打开失败" << endl;
throw - 1;
}
cout << "文件打开成功" << endl;
}
- 可以抛出类类型的对象
void bar(){
FILE * txt = fopen("./a.txt", "r");
if (!txt){
cout << "文件打开失败" << endl;
throw A(3);//以匿名临时对象抛出的异常,编译器会做优化,减少一次拷贝
}
cout << "文件打开成功" << endl;
}
- 不可以抛出局部对象的指针
void fu(){
FILE * txt = fopen("./a.txt", "r");
if (!txt){
cout << "文件打开失败" << endl;
A a = A(3);// 构造A
throw &a; // 这里会进行一次拷贝
}
cout << "文件打开成功" << endl;
}
解释:这是因为在c++中所有的异常都是抛向C++标准库中的,在标准库中会保存一份异常抛出对象的副本,而在异常抛出之后,原抛出异常的地方会进行销毁处理。
2.2 捕获异常
语法:try{}catch(…)
- 建议是在catch子句中使用引用接收异常对象,避免因拷贝构造带来的性能损失。
int main(){ try{ C08_01(); } catch (C08_B& b){// 这里就不会进行拷贝了,直接使用的是标准库里的对象 cout << "捕获到异常B" << endl; b.info(); } catch (C08_C& c){ cout << "捕获到异常C" << endl; c.info(); } catch (int & e){ cout << e << endl; } catch (C08_A a){// 这里会进行一次拷贝 a.info(); } cout << "程序执行成功" << endl; return 0; }
- 推荐使用匿名临时对象的形式抛出异常
```c++
void bar(){
FILE * txt = fopen("./a.txt", "r");
if (!txt){
cout << "文件打开失败" << endl;
throw A(3);//以匿名临时对象抛出的异常,编译器会做优化,减少一次拷贝
}
cout << "文件打开成功" << endl;
}
- 异常对象必须允许被拷贝构造和析构
2.3 匹配顺序
根据异常对象的类型自上而下的顺序匹配,而不是最优匹配,因此对子类类型异常的捕获不要放在基类类型异常的捕获后面。
3、异常说明
异常说明是函数原型的一部分,旨在说明函数可能抛出的异常类型。
- 语法格式:
返回类型 函数名(参数列表) throw(异常类型1,异常类型2,...){函数体}
- 异常说明是一种承诺,承诺函数不会抛出异常说明以外的异常类型。
-
如果函数抛出了异常说明以外的异常类型,那么该异常将无法捕获,会导致进程中止
-
std::unexpected()->std::terminate()->abort
-
隐式抛出异常的函数也可以列出它的异常说明
-
异常说明可以没有也可以为空
注意:
1:没有异常说明时,表示可能抛出任何类型的异常,比如void foo(){};代表foo这个函数可能抛出任何类型的异常
2:异常说明为空时,表示不会抛出任何类型的异常,比如void foo()throw(){} -
异常说明在函数的声明和定义中必须保持严格一致,否则将导致编译错误
- 忽略异常,不做处理
- 忽略异常,不做处理
-
4、构造函数中的异常
- 构造函数可以抛出异常,某些时候还必须抛出异常
- 构造过程中可能遇到各种错误,比如内存分配失败
- 构造函数没有返回值,无法通过返回值通知调用者
- 构造函数抛出异常,对象将被不完整构造,而一个被不完整构造的对象,其析构函数永远不会被执行
- 所有对象形式的成员变量,在抛出异常的瞬间,都能得到正确地析构(构造函数的回滚机制)
- 所有动态分配的资源,必须在抛出异常之前,自己手动释放,否则将形成资源的泄漏。
class C08_AA{
public:
C08_AA(){
cout << "AA类构造函数" << endl;
}
~C08_AA(){ cout << "AA类的析构函数" << endl; }
};
class C08_CC{
private:
C08_AA aa;
FILE *A;
public:
C08_CC(){
cout << "CC类构造函数" << endl;
A = fopen("./a.txt", "r");
if (!A){
// 对于动态申请的资源,必须自己手动释放
throw - 1;// 在构造函数中出现异常,那么所有对象形式的成员变量,在抛出异常的瞬间,都能得到正确地析构(构造函数的回滚机制)
}
}
~C08_CC(){
cout << "CC类析构函数" << endl;
}
};
int main(){
try{
C08_CC cc;// 如果cc是完整函数对象,则会调用cc的析构函数,如果cc是残缺对象,则不会调用cc的析构函数
}
catch(...){// 捕获任意异常
}
C08_CC cc;
return 0;
}
5、析构函数中的异常
不要在析构函数中主动抛出异常
- 析构函数只会在两种情况下被系统调用:
1:正常的销毁对象:当对象离开作用域或者显式的使用delete
2:异常的销毁对象:在异常传递的堆栈辗转开解过程中销毁对象 - 需要注意的是,对于第二种情况,此时系统中存在异常,而此时的析构函数中又抛出了异常,这时C++将通过std::terminate()函数,令进程中止
所以为了避免出现这种情况,在析构函数中,对于可能引发异常的操作,尽量在析构函数内部处理掉,不要主动抛出异常。
class C08_D{
public:
~C08_D(){
throw "析构函数抛出的异常";
}
};
int main(){
try{
C08_D d;
oneThrow();// 这里已经抛出了异常
}// 此时d中析构函数中也抛出了异常,此时这个异常就捕获不到,然后就会被系统杀死
catch (const char* a){
cout << a << endl;
}
catch (...){
}
}
6、标准库异常
- exception:抽象类
- runtime_error: 抽象类
- logic_error:抽象类
- overflow_error:上溢异常,一般用于容器满了的情况下
- underflow_error:下溢异常,一般用于容器空了的情况下
- invalid_argument:无效参数,一般用于实参和形参之间的数据不匹配
- length_error:长度错误
- out_of_range:超出范围
- bad_alloc:new操作符申请失败,会抛出这个异常
- bad_cast:动态类型转换中引用的转换失败,会抛出这个异常
- bad_type_id:使用typeid操作符获取信息失败时,会抛出这个异常