[c++]——异常
异常
c语言中处理出现的错误经常使用assert断言导致程序结束,或者使用错误码(程序员难以知道出错的方位),这对用户和程序员都会产生很多的不便之处,所以一起来看c++中处理错误的方式。
1.异常的概念
1.1异常的描述
异常:异常是一种处理错误的方式,是指一个函数在执行时出现了自己无法处理的错误将会抛出异常,来让函数的直接或者间接调用者来处理错误问题
1.2异常中的3个关键字
- throw:当程序发生一个错误时,用throw抛出一个异常
- try:简单理解为你所希望检查的代码段(可能抛出异常的代码)
- catch:在你想要处理问题的地方使用catch捕获异常,异常可以有多个catch关键字进行捕获
1.3语法示例
如果有一个块抛出一个异常,捕获异常的方法会使用 try 和 catch 关键字。try 块中放置可能抛出异常的代码,try 块中的代码被称为保护代码。
void Func2()
{
throw 异常对象
}
void Func1()
{
try
{
Func2();
//可能抛出异常的代码
}
catch (抛出异常对象的类型)
{}
}
2.异常的使用
2.1异常的抛出和捕获
1.异常是通过抛出对象而引发的,该对象的类型决定哪个catch代码块捕获异常
void Func1()
{
throw 5;//抛出int类型
}
int main()
{
try
{
Func1();
}
catch (string& s)
{
cout << s << endl;
}
catch (int i)//此代码块捕获异常
{
cout << i << endl;
}
return 0;
}
2.异常抛出后处理代码的catch块是离异常最近的且已经匹配的那一个
void Func2()
{
throw 5;
}
void Func1()
{
try
{
Func2();
}
catch (int i)//调用离抛出异常最近且匹配的catch块
{
cout << i << endl;
}
}
int main()
{
try
{
Func1();
}
catch (int i)
{
cout << i << endl;
}
return 0;
}
3.抛出异常后会生成一个异常对象的拷贝,因为抛出的异常对象可能是一个临时对象,这个临时对象会在被捕获后销毁
if(....)
{
string errmsg("打开文件失败");
throw errmsg;//抛出string类对象
}
4.catch(…)可以捕获任意类型的异常
catch (...)//可以捕获任何类型异常
{}
5.可以抛出的派生类对象,使用基类捕获,这个在实际中非常实用
2.2函数调用链中异常栈展开匹配原则
- 在抛出异常后,函数会先检查抛出异常部分的代码段是否在try块内部,如果是且找到了匹配的catch块则跳到catch块进行处理
- 如果在当前的函数栈中没有找到匹配的catch,则退出当前的函数栈,在调用的函数栈中接着匹配
- 如果到达main函数依旧没有匹配的catch块,函数则会终止,以上匹配栈展开就叫做异常栈展开原则,如果抛出的异常的没有被捕获程序将直接终止,不过我们一般会写上上面说的可以捕获任意类型的catch块
- 找到匹配的catch子句并处理以后,会继续沿着catch子句后面继续执行
2.3异常的重新抛出
具体场景可以如下:
double Division(int a, int b)
{
// 当b == 0时抛出异常
if (b == 0)
{
throw "Division by zero condition!";//此处抛出异常
}
return (double)a / (double)b;
}
void Func()
{
// 这里可以看到如果发生除0错误抛出异常,另外下面的array没有得到释放。
// 所以这里捕获异常后并不处理异常,异常还是交给外面处理,这里捕获了再
// 重新抛出去。
int* array = new int[10];
try {
int len, time;
cin >> len >> time;
cout << Division(len, time) << endl;
}
catch (...)//捕获异常处理释放问题,重新抛出
{
cout << "delete []" << array << endl;
delete[] array;
throw;
}
delete[] array;
}
int main()
{
try
{
Func();
}
catch (const char* errmsg)//捕获第二次抛出的异常
{
cout << errmsg << endl;
}
return 0;
}
3.异常安全
1.不要在构造函数中抛出异常,如果在构造函数中抛出有可能导致对象不完整或者对象没有完全的初始化。
2.不要在析构函数中抛出异常,如果在析构函数中抛出有可能导致造成资源泄露(内存泄漏)
3.在lock与unlock简直抛出异常会出现死锁问题
3.1异常规范
1.函数后接throw(类型列表。。。)表示函数会抛出类型列表中的一种
2.函数后接throw()表示函数不抛出异常
3.如果没有写任何声名表示可以抛出任何类型的异常
void Func()throw(A,B,C,D)//抛出类型列表中的一种
void Func()throw()//不抛出异常
void Func()//抛出任何类型的异常
4.自定义异常体系
大家知道派生类的对象可以通过切片行为赋值给基类的对象,在异常的体系中,通常我们抛出派生类的异常可以通过基类来进行捕获。
class A
{
protected:
string _errmsg;
int _id;
};
class B : public A
{};
class C : public A
{};
class D : public A
{};
int main()
{
try{
// 抛出对象都是派生类对象
}
catch (const A& e) // 这里捕获父类对象就可以
{}
catch (...)
{
cout << "Unkown Exception" << endl;
}
return 0;
}
5.c++库中异常体系
一个栗子:有兴趣的同学可以去http://www.cplusplus.com/reference/exception/exception/?kw=exception查看
int main()
{
try
{
string s("hello");
cout << s.at(7) << endl;//与operator[]访问元素行为相同
}
catch (exception& e)//使用基类对象捕获派生类
{
cout << e.what() << endl;//c++库中使用what()函数进行判断哪里出现错误
}
}
6.异常的优缺点(重点)
异常的优点
- 如果我们被抛出的异常对象已经定义好了,可以为我们精确的展现出错误的各种信息(甚至可以包括堆栈信息),这样可以帮助我们更好的定位bug
- 传统返回错误码的方式必须层层返回错误到达最外层,而抛异常可以直接到达捕获异常的最外层,使定位错误变得更快
- 很多的库和框架都应用了抛异常的方式
- 部分函数出错后使用抛异常更合理,比如operator[]如果发生了pos越界我们此时抛异常更合适而不是返回其他值
异常的缺点
- 异常会使程序的执行流乱跳,抛出异常时也会有同样的问题,非常混乱,会使我们调试分析问题时变得困难。
- 异常会造成一些性能的开销
- c++没有垃圾回收机制,资源需要自己管理。有了异常非常容易导致内存泄漏、死锁等异常安全问题
- c++的库中异常体系非常混乱,所以实际使用时一般都是用户自己定义
总结
异常尽量规范使用,否则后果不堪设想,随意抛异常,外层捕获的用户苦不堪言。所以异常规范有两点:一、抛出异常类型都继承自一个基类。二、函数是否抛异常、抛什么异常,都使用 func()throw();的方式规范化。
异常总体而言,利大于弊,所以工程中我们还是鼓励使用异常的。