- 异常机制的处理原理
原因:程序会出现错误,尤其是不易察觉的错误。需要了解并解决这些错误。
通常,程序出现错误,都会强制退出,很难排除错误原因
C语言的错误信息
- 函数返回值
- 通常,成功返回‘0’,失败返回‘-1’
- 返回值为指针类型,成功返回非‘NULL’,失败返回值为‘NULL’
- 其他另类的返回值:size_t ‘fread()’/‘fwrite()’
2.全局变量‘errno’
异常提供一个错误专用通道
优点:
- 不干扰正常的返回值
- 必须处理异常
- 函数返回值
#include <iostream>
#include <sstream>
using namespace std;
int main(int argc,char* argv[]){
istringstream iss(argv[1]);
int a[0];
iss >> a;
iss = argv[2];
int b(0);
iss >> b;
cout << a/b << endl;
}
‘try’-‘throw’-‘catch’
问题检测与问题解决分离
- 抛出异常
throw 表达式
- 捕获并处理异常
try{
包含可能抛出异常的语句;
}catch(类型名[形参名]){
处理异常
}
特点:
1. 只要抛出异常,异常后的代码不再执行
2. 异常的所抛出与经过的栈都会销毁
try{
包含可能抛出异常的语句;
}catch(类型1[形参名]){
处理异常
}catch(类型2[形参名]){
处理异常
}catch(类型3[形参名]){
处理异常
}catch(...){
处理异常
}
- 异常捕获具有类型匹配,只有相同的或者父类类型才能匹配到。
- 如果多个‘catch’都能接受相同异常,只有最前面的一个可以接收到。
- ‘catch(…)’只能放在所有异常捕获的最后。
- 异常的接口声明/异常规范
返回值类型 函数() throw(异常列表);
指定函数可以抛出何种异常,如果没有‘throw(异常列表)’默认可以抛出所有异常。
指定函数不抛出函数,异常列表为空’throw()‘。
注意事项
1. 如果抛出的异常一直没有函数捕获(catch),则会一直上传到C++运行系统那里,导致整个程序的终止
2. 一般在异常抛出后资源可以正常被释放,但注意如果在类的构造函数中抛出异常,系统是不会调用它的析构函数的,处理方式是:如果在构造函数中要抛出异常,则在抛出前要记得删除申请的资源。
3. 异常处理仅仅通过类型而不是通过值来(switch-case)匹配的,所以catch块的参数可以没有参数名称,只需要参数类型。
4. 函数原型中的异常说明要与实现中的异常说明一致,否则容易引起异常冲突。
5. 应该在throw语句后写上异常对象时,throw先通过Copy构造函数构造一个新对象,再把该新对象传递给catch。
那么当异常抛出后新对象如何释放?
异常处理机制保证:异常抛出的新对象并非创建在函数栈上,而是创建在专用的栈上,因此它才可以跨接多个函数而传递到上层,否则在栈清空的过程中就会被销毁。所有从try到throw语句之间起来的对象的析构函数将被自动调用。但如果一直上溯到main函数后还没有找到匹配的catch块,那么系统调用terminate()终止整个程序,这种情况下不能保证所有局部对象会被正确地销毁。
6. catch块的参数推荐采用地址传递而不是值传递,不仅可以提高效率,还可以利用对象的多态性。另外,派生类的异常捕获要放到父类异常捕获的前面,否则,派生类的异常无法被捕获。
7. 编写异常说明时,要确保派生类成员函数的异常说明和基类成员函数的异常说明一致,即派生类改写的虚函数的异常说明至少要和对应的基类虚函数的异常说明相同,甚至更加严格
#include <iostream>
using namespace std;
void test(){
cout << "before throw." << endl;
thow -1;
cout << "after throw." << endl;
}
int main(){
try{
test();
}catch(int a){
cout << "exception:" << a << endl;
}
}
#include <iostream>
using namespace std;
class TestZ{
public:
Test(){
cout << "Test Init "<< endl;
}
~Test(){
cout << "Test Destroy" << endl;
}
};
int main(){
try{
Test t;
cout << "before throw." << endl;
throw - 1;
cout << "after throw." <<endl;
}catch(int a){
cout << "exception:"<< a << endl;
}
}
- 标准异常类
- ‘exception’派生
异常类 | 作用 |
---|---|
‘logic_error’ | 逻辑错误,在程序运行前可以检测出来 |
‘runtime_error’ | 运行时错误,仅在程序运行中检测到 |
* 逻辑异常‘logic_error’诞生
异常类 | 作用 |
---|---|
‘domain_error’ | 违反了前置条件 |
‘invalid_argument’ | 指出函数的一个无效参数 |
‘length_error’ | 指出有一个超出类型‘size_t’的最大可表现值长度的对象的企图 |
‘out_of_range’ | 参数越界 |
‘bad_cast’ | 在运行时类型识别中有一个无效的‘dynamic_cast’的表达式 |
‘bad_typeid’ | 报告在表达式‘typeid(*p)’中有一个空指针‘p’ |
- 运行时‘runtime_error’派生
异常类 | 作用 |
---|---|
‘range_error’ | 违反后置条件 |
‘bad_alloc’ | 存储分配错误 |
尝试捕获逻辑异常和运行时异常
- 自定义异常类
- 编码流程
- 继承异常类‘exception’
- 实现接口‘what()’
- 代码结构
class 异常类:public exception{
public:
const char* what()const throw(){
return 信息字符串;
}
};
构造函数、析构函数的异常处理
- 构造函数可以抛出异常,此时不用调用析构函数,所以如果抛出异常前,申请了资源,需要自己释放。
- C++标准指明析构函数不能、也不应该抛出异常。
- 如果析构函数抛出异常,则异常点之后的程序不会执行,如果析构函数在异常点之后执行了某些必要的动作比如释放某些资源,则这些动作不会执行,会造成诸如资源泄露的问题。
- 通常异常发生时,C++的机制会调用已经构造对象的析构函数来释放资源,此时若析构函数本身也抛出异常,则前一个异常尚未处理,又有新的异常,会造成程序崩溃的问题。
g++ -fno-exceptions
在不同的编码规范中,对是否使用异常存在争议。