异常处理和捕获总结
程序员文章站
2022-05-23 11:06:51
...
1、为什么要使用异常处理?
因为健壮性是非常重要的软件质量属性,它是软件在异常情况下依然可以运行的能力,因此在错误即将发生前通过检测触发它的条件来阻止它,从而防止造成危害,是软件正常运行。将错误扼杀在摇篮里。
1.1、传统的异常处理方法
- 1.1.1:函数返回彼此协商后的状态编码来表示操作成功或失败、以及其他不同类型的错误
- 1.1.2:使用全局变量来保存错误编码,每一个使用它的函数开始时就检查它的值,并且每一个函数的操作结果都写到这个全局变量中
- 1.1.3:出错时终止程序
但是这些方法依然存在很大缺点,状态编码和错误编码难以形成统一的标准,并且每一个状态码的返回都必须定义一个用户可以理解的错误状态信息,可以想象这是一个复杂的工作,并且还会增大代码体积,甚至降低程序的可读性,并且有些函数就没有返回值,例如构造函数和析构函数,你不能保证这两个函数一定正确吧,因此传统方法就存在了局限性,为了解决这些问题,C++就出现了异常处理机制
1.2、异常处理
1.2.1、异常处理机制的原理
在真正导致错误的语句发送之前,并且异常发生的条件已经具备了,使用我们自定义的的软件异常或者系统的软件异常来代替它,从而阻止它,因此当异常抛出时,真正的错误实际上还没有发生。
eg1:
void Test()
{
try
{
char *p = new char[0x7fffffff];//抛异常
}
catch (exception e)//exception是系统定义的类型
{
cout << e.what() << endl;//捕获异常,告诉程序出错原因
}
}
int main()
{
Test();
system("pause");
return 0;
}
这样就会打印出来捕获异常的结果,方便自己知道自己错误在哪里,这些都是系统内部定义的一些异常处理函数。
1.2.2、异常处理机制的语法结构
try{}块包含可能会有异常抛出的代码段
catch{}块包含用户自定义的异常处理代码
throw是一条语句,用于抛出异常
需要注意的是:一条throw语句只能抛出一条异常,一个catch子句也只能捕获一种异常,异常捕获必须和try块结合使用,并且可以通过异常组合在一个地点捕获多种异常。
eg2:
class A
{};
void Test()
{
try
{
throw 10;//抛出点在当前try块内
throw A();
}
catch (exception e)
{
cout << e.what() << endl;//捕获异常,告诉程序出错原因
}
catch (int a)
{
cout << a << endl;
}
catch (...)//捕获其它所有可能的异常
{
cout << "Unexpected exception occurred!" << endl;
}
}
上面代码中throw 10会匹配到第一个catch语句,而throw A()语句找不到匹配的就会匹配最终catch(…)语句。因此
- 1、 异常是通过抛出对象而引发的,该对象的类型决定了应该**哪个处理代码。
- 2、被选中的处理代码是调用链中与该对象类型匹配且离抛出异常位置最近的那一个。
- 3、对于每一个抛出的异常,总能找到一个对应的throw语句,一些在是我们自己定义的语句,一些是标准库之中的。就像第一个例子,没有throw语句,但是catch到了异常。
- 4、由于异常处理机制采用的是类型匹配而不是值判断,因此在catch语句中只需要参数类型,除非确实要使用那个异常对象。
1.2.3、异常的类型匹配规则
- 1、catch子句的参数类型就是异常对象的类型或引用
- 2、如果catch子句类型为public基类指针,而异常对象则为派生类指针
- 3、catch子句参数类型为void*,而异常对象类型可为所类型
- 4、catch语句可以为catch(…),代表捕获所有可能的异常
- 5、允许const对象转为非const对象
- 6、将数组转化为指向数组类型的指针,将函数转化为指向函数类型的指针
class Exception
{
public:
Exception(int errId , char * errMsg)
: _errId(errId)
, _errMsg(errMsg)
{}
void What() const
{
printf("errId:\n", _errId);
printf("errMsg:%s\n", _errMsg);
}
private:
int _errId; // 错误码
string _errMsg; // 错误消息
};
void Func1(bool isThrow)
{
if (isThrow)
{
throw Exception(1, "抛出 Excepton对象");
}
printf("Func1(%d)\n", isThrow);
}
void Func2(bool isThrowString, bool isThrowInt)
{
if (isThrowString)
{
throw string("抛出 string对象");
}
if (isThrowInt)
{
throw 7;
}
printf("Func2(%d, %d)\n", isThrowString, isThrowInt);
}
void Func()
{
try
{
Func1(true);
Func2(false, true);
}
catch (const string& errMsg)
{
printf("Catch string Object:", errMsg);
}
catch (int errId)
{
printf("Catch string Object:", errId);
}
catch (const Exception& e)
{
e.What();
}
catch (...)
{
cout << " 未知异常" << endl;
}
printf("Func()\n");
}
int main()
{
Func();
system("pause");
return 0;
}
Func1和Func2中的false和true位置不一样,以及两个函数顺序不一样都会得到不一样的异常处理,根据抛出的异常匹配到最适合的异常捕获。
1.2.4、异常的重新抛出
概念:有可能单个的catch不能处理一个异常,在对异常进行一些校订处理后,希望交给更上层的函数来处理,catch则可以通过重新抛出将异常传递给更上层的函数进行处理
class Exception
{
public:
Exception(int errId = 0, const char * errMsg = "")
: _errId(errId)
, _errMsg(errMsg)
{}
void What() const
{
printf("errId:\n", _errId);
printf("errMsg:%s\n", _errMsg);
}
private:
int _errId; // 错误码
string _errMsg; // 错误消息
};
void Func1()
{
throw string("Throw Func1 string");
}
void Func2()
{
try
{
Func1();
}
catch (Exception & e)
{
e.What();
}
}
void Func3()
{
try
{
Func2();
}
catch (string & errMsg)
{
printf("errMsg throw\n");
}
}
int main()
{
Func3();
system("pause");
return 0;
}
Func1中抛出了异常,到Func2中没有匹配到捕获异常函数,继续向上层函数抛出,直到Func3捕获到异常
1.3、那些函数最好不要抛出异常
- 构造函数完成对象的构造和初始化,需要保证不要在构造函数中抛出异常,否则可能导致对象不完整或没有完全初始化。
- 析构函数主要完成资源的清理,需要保证不要在析构函数内抛出异常,否则可能导致资源泄漏(内存泄漏、句柄未关闭等)
1.4、异常处理中注意事项
- 1.4.1:catch块的参数应当采用引用传递而不是值传递
- 原因1:异常对象可能在调用链函数中要回溯好几层才可以匹配到处理块,所以传引用比值传递效率高很多
- 原因2:因为异常类型可能是多态类,你可以抛出一个异常对象的地址,那么catch块中的参数就是异常类型的指针了
- 1.4.2:在异常组合中,要把派生类的异常捕获放在基类前面,否则派生类将无法捕获到派生类的异常
- 1.4.3:全局对象在程序开始运行前进行构造,如果构造函数抛出异常,将永远无法捕获,析构也是如此,因为它们在程序结束后才会被调用,这些异常只有操作系统才可以捕获,应用程序没有办法
- 1.4.4:对于局部对象,异常处理机制是所有从try到throw语句之间构造起来的局部对象的析构函数将被自动调用,然后清退栈堆(就行main函数退出那样),如果一直回溯到main函数后还是没有匹配到catch块,那么系统会调用terminate()终止整个程序,在这种情况下就不能保证局部对象会被正确的销毁了
上一篇: Roll A Ball
下一篇: Java定时任务调度