C 异常处理机制总结
参考文档:《C 编程思想》《C Primer》《More effective C 》
一、 传统的错误处理机制:
1. 返回值或全局错误状态标志。
缺点:需要冗长的错误检查代码。
2. C standard Library中的信号处理系统,signal函数。
缺点:信号处理机制比较复杂;耦合度高;复杂系统中的信号容易产生冲突。
3. C standard Library中的非局部跳转函数,setjmp和longjmp。
缺点: C 的类析构函数不会被调用,对象不能正确被清理,造成内存泄漏;耦合度高。
二、 C 异常处理机制介绍:
1. 语法:
try
{
throw 3.0;
}
catch (int) //如果处理块内不需要该变量,可以省略形参
{
cout << "int exception happened!" << endl;
}
catch (double d)
{
cout << d << " double exception happened!" << endl;
}
catch (…)
{
cout << "unknown exception happened!" << endl;
}
2. 说明:
1) throw 可以抛出一个任意类型的变量(或表达式)(包括内置类型如int的变量,或者自定义的类型的变量),catch 按照被抛出变量的编译时类型进行匹配,找到第一个匹配的类型即进入异常处理。
2) 异常处理是为了保证使程序能够不异常退出。
3) 建议:尽量不要使用throw抛出内置类型的变量。
4) 如果throw抛出的异常找不到匹配的类型,最终程序将调用C standard Library的terminate函数,程序将异常退出。
5) 当程序跳出try块时,try块内的局部变量被自动释放,对象的析构函数被调用。所以,为了保证程序不异常退出,应该保证析构函数不会抛出异常。
6) 调用函数时,程序的控制权最终还会返回到函数的调用处,但是当你抛出一个异常时,控制权永远不会回到抛出异常的地方。
7) 异常匹配时,只允许三种类型转换:const与非const;派生类与基类;数组与指针。(注意:不允许算术转换.)
8) 建议:catch子句的次序必须反映类型层次,派生类放到基类前面。
9) throw出的对象称为异常对象(exception object),由编译器管理,catch接受到的对象如果不是引用或指针的话,则进行对象拷贝。但是异常对象是程序结束才被释放。
10) 异常可以发生在构造函数中或者构造函数初始化式中。注意:如果异常发生在构造函数中,对象的析构函数将不会被调用!所以需要在构造函数中进行try-catch自己释放资源。另外,为了处理构造函数初始化式中可能发生的异常,语法应该修改为如下:
//normal constructor
UserClass(): m_nA(1), m_nB(2) { /*constructor body*/ }
//constructor using function try block
UserClass() try: m_nA(1), m_nB(2){ /*constructor body*/ }catch(…){ }
11) 标准库异常类定义在<exception><stdexcep>头文件中。
3. 重新抛出:
在catch块或被catch块调用的函数中,可以用”throw;”语句(throw空对象)将异常重新抛出。
4. 异常规格说明(exception specification):
说明函数将会抛出什么类型的异常。
例子:
void func(int i) throw (runtime_error); //说明该函数func有可能抛出runtime_error异常
void func(int i) throw(); //说明函数func不会抛出任何异常
1) 如果函数运行时抛出了其他类型的异常,程序将会调用标准库的unexpected函数,该函数将调用terminate退出程序。
2) 派生类的虚函数的异常规格说明只能和基类一样或比基类更严格,不能增加新的异常类型。
3) 注意:一般只会使用throw()来说明一个函数是安全的,不会抛出任何异常,这样编译器就可以对调用该函数的代码做出优化。一般不会使用其他的异常规格说明。
5. 标准异常库:(见<exception><stdexcep><new><typeinfo>头文件)
其中,类exception的定义如下:
class exception
{
public:
exception();
exception(const char* const &);
exception(const exception&);
exception& operator= (const exception&);
virtual ~exception();
virtual char* what() const; //获得信息
private:
const char* _m_what;
int _m_doFree;
};
三、 使用异常处理机制的建议:
来自《C 编程思想》和《More Effective C 》的建议:
1. 不要使用异常的情形:
1) 绝对不要在异步事件中使用异常。如使用了信号机制的系统、中断处理程序等。
2) 不要在处理简单错误的时候使用异常。一般情况下,自己可以处理的错误就直接处理,只有当在此处处理不了的错误才抛出到更大的语境中。
3) 不要将异常用于流程控制,比如代替switch语句。因为异常处理效率非常低,而且编译器会做出很多程序员不知道的事情。
4) 遇到不可恢复的错误时,最好不用处理异常,直接将异常交给操作系统处理即可。
2. 应该使用异常的情形:
1) 遇到可以修正的错误,通过一些行为可以使程序继续执行;
2) 在当前的语境中不能完全处理的错误,但有可能在较高层的语境中可以处理,这时,可以通过将一个同样类型或者不同类型的异常抛出;
3) 发生不足以让程序退出的错误,但是你认为该错误是致命错误的时候,可以通过抛出异常便于及时发现故障而终止程序;
4) 为了简化错误处理。
3. 对使用异常的建议(1,4,5,6见MoreEffectiveC ):
1) 慎重使用异常规格说明:当不确定函数抛出何种异常时,最好不要使用异常规格说明。
2) 尽量使用标准异常库的异常类:编写自定义的异常类之前,先查看标准异常库。如果标准异常库没有需要的语义的异常,尽量从标准异常类派生出自定义的异常类。
3) 建立自己的异常类层次结构。
4) 尽量通过引用来捕捉异常,原因有二:防止对象拷贝;防止对象slicing。异常对象由编译器统一管理,可以放心使用引用来操作。
5) 可以在构造函数中抛出异常:如果构造函数发生故障而没有及时发现,程序继续运行会造成不可预料的灾难性结果,这种情况下可以在构造函数中抛出异常。
6) 不要在析构函数中抛出异常。所以有时候需要在析构函数中处理异常。
四、 编码规范:
1. 函数的参数检查不合法时,抛出标准库的invalid_argument(logic_error子类)异常。
2. 调用函数时,应该检查logic_error异常,并做相应处理,该异常不足以使程序终止。
3. 发生致命逻辑错误或者不可恢复错误时,不要捕捉抛出的异常,直接将其交给操作系统处理,退出程序。
4. 尽量使用标准异常库的异常类,没有合适的标准异常时,建立自己的异常层次结构。
5. 自定义异常类时,应该从标准异常库中的类派生,异常类的名称应该能反映出错误的类型。
6. 使用引用来捕捉异常。
7. 不要使用catch(…)。
8. 捕捉到未知异常时,要重新抛出,以免发生故障后继续运行程序,造成不可预料的后果。
9. 尽量将资源释放写入析构函数,这样防止发生异常时,资源不能释放。
10. 不要使用异常规格说明。??讨论??
11. 建立一个系统的异常基类,将所有系统中发生的异常都封装成该基类的子类。 ??讨论??
五、 例子:
#include <iostream>
#include <exception>
using namespace std;
void func(int i)
{
if (i>100)
{
throw invalid_argument("invalid argument: argument can not be bigger than 100");
}
//do something…
}
int main()
{
int i;
cout << "Please input a number less than 100:" << endl;
cin >> i;
try
{
func(i);
}
catch (logic_error& e) //caught by reference
{
cout << "invalid input: " << e.what() << endl; //logic_error, can be handled, do not need to abort
}
catch (exception&)
{
//do something…
throw; //unknown exception caught, throw again
}
return 0;
}
六、 对使用new操作符分配内存失败的说明:
较新的C 标准中规定,普通的new操作符失败时,抛出std::bad_alloc异常。但是在以前,new失败是简单的返回一个NULL指针。在一定的环境下,返回一个NULL指针来表示一个失败依然是一个不错的选择。C 标准委员会意识到这个问题,所以他们决定定义一个特别的new操作符版本,这个版本返回0表示失败。这就是nothrow new。
nothrow new只是简单的给operator new加了一个参数。
相关代码如下:
class nothrow_t // in namespace std
{}; //empty class
extern const nothrow_t nothrow; //an object , in namespace std
//declarations from <new>
void * operator new (size_t size, const std::nothrow_t &);
//array version
void * operator new[] (size_t size, const std::nothrow_t &);
所以,使用nothrow new时,可以如下使用:
#include <new>
#include <iostream> // for std::cerr
#include <cstdlib> // for std::exit()
Task * ptask = new (std::nothrow) Task;
if (!ptask)
{
std::cerr<<"allocation failure!";
std::exit(1);
}
也可以创建你自己的nothrow_t对象来完成相同的效应:
#include <new>
std::nothrow_t nt;
Task * ptask = new (nt) Task; //user-defined argument
if (!ptask)
//...
值得一提的是,在VC6和VC2005编译器下,默认的new操作符失败的行为也是简单的返回一个NULL指针,它们并不是遵循C 标准抛出一个std::bad_alloc异常。
————————————————
通过分享实用的计算机编程语言干货,推动中国编程到2025年基本实现普及化,使编程变得全民皆知,最终实现中国编程之崛起,这里是中国编程2025,感谢大家的支持。
原文链接:https://blog.csdn.net/MulinB/article/details/1763240
,