c++异常安全
异常处理
增强错误恢复能力是提高代码健壮性的最有力的途径之一,C语言中采用的错误处理方法被认为是紧耦合的,函数的使用者必须在非常靠近函数调用的地方编 写错误处理代码,这样会使得其变得笨拙和难以使用。C++中引入了异常处理机制,这是C++的主要特征之一,是考虑问题和处理错误的一种更好的方式。使用 错误处理可以带来一些优点,如下:
-
错误处理代码的编写不再冗长乏味,并且不再和正常的代码混合在一起,程序员只需要编写希望产生的代码,然后在后面某个单独的区段里编写处理错误的嗲吗。多次调用同一个函数,则只需要某个地方编写一次错误处理代码。
-
错误不能被忽略,如果一个函数必须向调用者发送一次错误信息。它将抛出一个描述这个错误的对象。
传统的错误处理和异常处理
在讨论异常处理之前,我们先谈谈C语言中的传统错误处理方法,这里列举了如下三种:
-
在函数中返回错误,函数会设置一个全局的错误状态标志。
-
使用信号来做信号处理系统,在函数中raise信号,通过signal来设置信号处理函数,这种方式耦合度非常高,而且不同的库产生的信号值可能会发生冲突
-
使用标准C库中的非局部跳转函数 setjmp和longjmp ,这里使用setjmp和longjmp来演示下如何进行错误处理:
- #include <iostream>
- #include <setjmp.h>
- jmp_buf static_buf; //用来存放处理器上下文,用于跳转
- void do_jmp()
- {
- //do something,simetime occurs a little error
- //调用longjmp后,会载入static_buf的处理器信息,然后第二个参数作为返回点的setjmp这个函数的返回值
- longjmp(static_buf,10);//10是错误码,根据这个错误码来进行相应的处理
- }
- int main()
- {
- int ret = 0;
- //将处理器信息保存到static_buf中,并返回0,相当于在这里做了一个标记,后面可以跳转过来
- if((ret = setjmp(static_buf)) == 0) {
- //要执行的代码
- do_jmp();
- } else { //出现了错误
- if (ret == 10)
- std::cout << "a little error" << std::endl;
- }
- }
错误处理方式看起来耦合度不是很高,正常代码和错误处理的代码分离了,处理处理的代码都汇聚在一起了。但是基于这种局部跳转的方式来处理代码,在 C++中却存在很严重的问题,那就是对象不能被析构,局部跳转后不会主动去调用已经实例化对象的析构函数。这将导致内存泄露的问题。下面这个例子充分显示 了这点
- #include <iostream>
- #include <csetjmp>
- using namespace std;
- class base {
- public:
- base() {
- cout << "base construct func call" << endl;
- }
- ~base() {
- cout << "~base destruct func call" << endl;
- }
- };
- jmp_buf static_buf;
- void test_base() {
- base b;
- //do something
- longjmp(static_buf,47);//进行了跳转,跳转后会发现b无法析构了
- }
- int main() {
- if(setjmp(static_buf) == 0) {
- cout << "deal with some thing" << endl;
- test_base();
- } else {
- cout << "catch a error" << endl;
- }
- }
在上面这段代码中,只有base类的构造函数会被调用,当longjmp发生了跳转后,b这个实例将不会被析构掉,但是执行流已经无法回到这里,b 这个实例将不会被析构。这就是局部跳转用在C++中来处理错误的时候带来的一些问题,在C++中异常则不会有这些问题的存在。那么接下来看看如何定义一个 异常,以及如何抛出一个异常和捕获异常吧.
异常的抛出
- class MyError {
- const char* const data;
- public:
- MyError(const char* const msg = 0):data(msg)
- {
- //idle
- }
- };
- void do_error() {
- throw MyError("something bad happend");
- }
- int main()
- {
- do_error();
- }
上面的例子中,通过throw抛出了一个异常类的实例,这个异常类,可以是任何一个自定义的类,通过实例化传入的参数可以表明发生的错误信息。其实 异常就是一个带有异常信息的类而已。异常被抛出后,需要被捕获,从而可以从错误中进行恢复,那么接下来看看如何去捕获一个异常吧。在上面这个例子中使用抛 出异常的方式来进行错误处理相比与之前使用局部跳转的实现来说,最大的不同之处就是异常抛出的代码块中,对象会被析构,称之为堆栈反解.
异常的捕获
C++中通过catch关键字来捕获异常,捕获异常后可以对异常进行处理,这个处理的语句块称为异常处理器。下面是一个简单的捕获异常的例子:
- try{
- //do something
- throw string("this is exception");
- } catch(const string& e) {
- cout << "catch a exception " << e << endl;
- }
catch有点像函数,可以有一个参数,throw抛出的异常对象,将会作为参数传递给匹配到到catch,然后进入异常处理器,上面的代码仅仅是 展示了抛出一种异常的情况,加入try语句块中有可能会抛出多种异常的,那么该如何处理呢,这里是可以接多个catch语句块的,这将导致引入另外一个问 题,那就是如何进行匹配。
异常的匹配
异常的匹配我认为是符合函数参数匹配的原则的,但是又有些不同,函数匹配的时候存在类型转换,但是异常则不然,在匹配过程中不会做类型的转换,下面的例子说明了这个事实:
- #include <iostream>
- using namespace std;
- int main()
- {
- try{
- throw 'a';
- }catch(int a) {
- cout << "int" << endl;
- }catch(char c) {
- cout << "char" << endl;
- }
- }
上面的代码的输出结果是char,因为抛出的异常类型就是char,所以就匹配到了第二个异常处理器。可以发现在匹配过程中没有发生类型的转换。将 char转换为int。尽管异常处理器不做类型转换,但是基类可以匹配到派生类这个在函数和异常匹配中都是有效的,但是需要注意catch的形参需要是引 用类型或者是指针类型,否则会 导致切割派生类这个问题。
- //基类
- class Base{
- public:
- Base(string msg):m_msg(msg)
- {
- }
- virtual void what(){
- cout << m_msg << endl;
- }
- void test()
- {
- cout << "I am a CBase" << endl;
- }
- protected:
- string m_msg;
- };
- //派生类,重新实现了虚函数
- class CBase : public Base
- {
- public:
- CBase(string msg):Base(msg)
- {
- }
- void what()
- {
- cout << "CBase:" << m_msg << endl;
- }
- };
- int main()
- {
- try {
- //do some thing
- //抛出派生类对象
- throw CBase("I am a CBase exception");
- }catch(Base& e) { //使用基类可以接收
- e.what();
- }
- }
上面的这段代码可以正常的工作,实际上我们日常编写自己的异常处理函数的时候也是通过继承标准异常来实现字节的自定义异常的,但是如果将 Base&换成Base的话,将会导致对象被切割,例如下面这段代码将会编译出错,因为CBase被切割了,导致CBase中的test函数无法 被调用。
- try {
- //do some thing
- throw CBase("I am a CBase exception");
- }catch(Base e) {
- e.test();
- }
到此为此,异常的匹配算是说清楚了,总结一下,异常匹配的时候基本上遵循下面几条规则:
异常匹配除了必须要是严格的类型匹配外,还支持下面几个类型转换.
-
允许非常量到常量的类型转换,也就是说可以抛出一个非常量类型,然后使用catch捕捉对应的常量类型版本
-
允许从派生类到基类的类型转换
-
允许数组被转换为数组指针,允许函数被转换为函数指针
假想一种情况,当我要实现一代代码的时候,希望无论抛出什么类型的异常我都可以捕捉到,目前来说我们只能写上一大堆的catch语句捕获所有可能在 代码中出现的异常来解决这个问题,很显然这样处理起来太过繁琐,幸好C++提供了一种可以捕捉任何异常的机制,可以使用下列代码中的语法。
catch(...) {
//异常处理器,这里可以捕捉任何异常,带来的问题就是无法或者异常信息
}
如果你要实现一个函数库,你捕捉了你的函数库中的一些异常,但是你只是记录日志,并不去处理这些异常,处理异常的事情会交给上层调用的代码来处理.对于这样的一个场景C++也提供了支持.
- try{
- throw Exception("I am a exception");
- }catch(...) {
- //log the exception
- throw;
- }
通过在catch语句块中加入一个throw,就可以把当前捕获到的异常重新抛出.在异常抛出的那一节中,我在代码中抛出了一个异常,但是我没有使用任何catch语句来捕获我抛出的这个异常,执行上面的程序会出现下面的结果.
- terminate called after throwing an instance of 'MyError'
- Aborted (core dumped)
为什么会出现这样的结果呢?,当我们抛出一个异常的时候,异常会随着函数调用关系,一级一级向上抛出,直到被捕获才会停止,如果最终没有被捕获将会 导致调用terminate函数,上面的输出就是自动调用terminate函数导致的,为了保证更大的灵活性,C++提供了set_terminate 函数可以用来设置自己的terminate函数.设置完成后,抛出的异常如果没有被捕获就会被自定义的terminate函数进行处理.下面是一个使用的 例子:
- #include <exception>
- #include <iostream>
- #include <cstdlib>
- using namespace std;
- class MyError {
- const char* const data;
- public:
- MyError(const char* const msg = 0):data(msg)
- {
- //idle
- }
- };
- void do_error() {
- throw MyError("something bad happend");
- }
- //自定义的terminate函数,函数原型需要一致
- void terminator()
- {
- cout << "I'll be back" << endl;
- exit(0);
- }
- int main()
- {
- //设置自定义的terminate,返回的是原有的terminate函数指针
- void (*old_terminate)() = set_terminate(terminator);
- do_error();
- }
- 上面的代码会输出I'll be back
到此为此关于异常匹配的我所知道的知识点都已经介绍完毕了,那么接着可以看看下一个话题,异常中的资源清理.
异常中的资源清理
在谈到局部跳转的时候,说到局部调转不会调用对象的析构函数,会导致内存泄露的问题,C++中的异常则不会有这个问题,C++中通过堆栈反解将已经 定义的对象进行析构,但是有一个例外就是构造函数中如果出现了异常,那么这会导致已经分配的资源无法回收,下面是一个构造函数抛出异常的例子:
- #include <iostream>
- #include <string>
- using namespace std;
- class base
- {
- public:
- base()
- {
- cout << "I start to construct" << endl;
- if (count == 3) //构造第四个的时候抛出异常
- throw string("I am a error");
- count++;
- }
- ~base()
- {
- cout << "I will destruct " << endl;
- }
- private:
- static int count;
- };
- int base::count = 0;
- int main()
- {
- try{
- base test[5];
- } catch(...){
- cout << "catch some error" << endl;
- }
- }
- 上面的代码输出结果是:
- I start to construct
- I start to construct
- I start to construct
- I start to construct
- I will destruct
- I will destruct
- I will destruct
- catch some error
在上面的代码中构造函数发生了异常,导致对应的析构函数没有执行,因此实际编程过程中应该避免在构造函数中抛出异常,如果没有办法避免,那么一定要 在构造函数中对其进行捕获进行处理.最后介绍一个知识点就是函数try语句块,如果main函数可能会抛出异常该怎么捕获?,如果构造函数中的初始化列表 可能会抛出异常该怎么捕获?下面的两个例子说明了函数try语句块的用法:
- #include <iostream>
- using namespace std;
- int main() try {
- throw "main";
- } catch(const char* msg) {
- cout << msg << endl;
- return 1;
- }
- main函数语句块,可以捕获main函数中抛出的异常.
- class Base
- {
- public:
- Base(int data,string str)try:m_int(data),m_string(str)//对初始化列表中可能会出现的异常也会进行捕捉
- {
- // some initialize opt
- }catch(const char* msg) {
- cout << "catch a exception" << msg << endl;
- }
- private:
- int m_int;
- string m_string;
- };
- int main()
- {
- Base base(1,"zhangyifei");
- }
上面说了很多都是关于异常的使用,如何定义自己的异常,编写异常是否应该遵循一定的标准,在哪里使用异常,异常是否安全等等一系列的问题,下面会一一讨论的.
标准异常
C++标准库给我们提供了一系列的标准异常,这些标准异常都是从exception类派生而来,主要分为两大派生类,一类是 logic_error,另一类则是runtime_error这两个类在stdexcept头文件中,前者主要是描述程序中出现的逻辑错误,例如传递了 无效的参数,后者指的是那些无法预料的事件所造成的错误,例如硬件故障或内存耗尽等,这两者都提供了一个参数类型为std::string的构造函数,这 样就可以将异常信息保存起来,然后通过what成员函数得到异常信息.
- #include <stdexcept>
- #include <iostream>
- #include <string>
- using namespace std;
- class MyError:public runtime_error {
- public:
- MyError(const string& msg = "") : runtime_error(msg) {}
- };
- //runtime_error logic_error 两个都是继承自标准异常,带有string构造函数
- //
- int main()
- {
- try {
- throw MyError("my message");
- } catch(MyError& x) {
- cout << x.what() << endl;
- }
- }
异常规格说明
假设一个项目中使用了一些第三方的库,那么第三方库中的一些函数可能会抛出异常,但是我们不清楚,那么C++提供了一个语法,将一个函数可能会抛出 的异常列出来,这样我们在编写代码的时候参考函数的异常说明即可,但是C++11中这中异常规格说明的方案已经被取消了,所以我不打算过多介绍,通过一个 例子看看其基本用法即可,重点看看C++11中提供的异常说明方案:
- #include <exception>
- #include <iostream>
- #include <cstdio>
- #include <cstdlib>
- using namespace std;
- class Up{};
- class Fit{};
- void g();
- //异常规格说明,f函数只能抛出Up 和Fit类型的异常
- void f(int i)throw(Up,Fit) {
- switch(i) {
- case 1: throw Up();
- case 2: throw Fit();
- }
- g();
- }
- void g() {throw 47;}
- void my_ternminate() {
- cout << "I am a ternminate" << endl;
- exit(0);
- }
- void my_unexpected() {
- cout << "unexpected exception thrown" << endl;
- // throw Up();
- throw 8;
- //如果在unexpected中继续抛出异常,抛出的是规格说明中的 则会被捕捉程序继续执行
- //如果抛出的异常不在异常规格说明中分两种情况
- //1.异常规格说明中有bad_exception ,那么会导致抛出一个bad_exception
- //2.异常规格说明中没有bad_exception 那么会导致程序调用ternminate函数
- // exit(0);
- }
- int main() {
- set_terminate(my_ternminate);
- set_unexpected(my_unexpected);
- for(int i = 1;i <=3;i++)
- {
- //当抛出的异常,并不是异常规格说明中的异常时
- //会导致最终调用系统的unexpected函数,通过set_unexpected可以
- //用来设置自己的unexpected汗函数
- try {
- f(i);
- }catch(Up) {
- cout << "Up caught" << endl;
- }catch(Fit) {
- cout << "Fit caught" << endl;
- }catch(bad_exception) {
- cout << "bad exception" << endl;
- }
- }
- }
上面的代码说明了异常规格说明的基本语法,以及unexpected函数的作用,以及如何自定义自己的unexpected函数,还讨论了在 unexpected函数中继续抛出异常的情况下,该如何处理抛出的异常.C++11中取消了这种异常规格说明.引入了一个noexcept函数,用于表 明这个函数是否会抛出异常
void recoup(int) noexecpt(true); //recoup不会抛出异常
void recoup(int) noexecpt(false); //recoup可能会抛出异常
此外还提供了noexecpt用来检测一个函数是否不抛出异常.
异常安全
异常安全我觉得是一个挺复杂的点,不光光需要实现函数的功能,还要保存函数不会在抛出异常的情况下,出现不一致的状态.这里举一个例子,大家在实现 堆栈的时候经常看到书中的例子都是定义了一个top函数用来获得栈顶元素,还有一个返回值是void的pop函数仅仅只是把栈顶元素弹出,那么为什么没有 一个pop函数可以 即弹出栈顶元素,并且还可以获得栈顶元素呢?
- template<typename T> T stack<T>::pop()
- {
- if(count == 0)
- throw logic_error("stack underflow");
- else
- return data[--count];
- }
如果函数在最后一行抛出了一个异常,那么这导致了函数没有将退栈的元素返回,但是Count已经减1了,所以函数希望得到的栈顶元素丢失了.本质原 因是因为这个函数试图一次做两件事,1.返回值,2.改变堆栈的状态.最好将这两个独立的动作放到两个独立的函数中,遵守内聚设计的原则,每一个函数只做 一件事.我们 再来讨论另外一个异常安全的问题,就是很常见的赋值操作符的写法,如何保证赋值操作是异常安全的.
- class Bitmap {...};
- class Widget {
- ...
- private:
- Bitmap *pb;
- };
- Widget& Widget::operator=(const Widget& rhs)
- {
- delete pb;
- pb = new Bitmap(*rhs.pb);
- return *this;
- }
上面的代码不具备自我赋值安全性,倘若rhs就是对象本身,那么将会导致*rhs.pb指向一个被删除了的对象.那么就绪改进下.加入证同性测试.
- Widget& Widget::operator=(const Widget& rhs)
- {
- If(this == rhs) return *this; //证同性测试
- delete pb;
- pb = new Bitmap(*rhs.pb);
- return *this;
- }
但是现在上面的代码依旧不符合异常安全性,因为如果delete pb执行完成后在执行new Bitmap的时候出现了异常,则会导致最终指向一块被删除的内存.现在只要稍微改变一下,就可以让上面的代码具备异常安全性.
- Widget& Widget::operator=(const Widget& rhs)
- {
- If(this == rhs) return *this; //证同性测试
- Bitmap *pOrig = pb;
- pb = new Bitmap(*rhs.pb); //现在这里即使发生了异常,也不会影响this指向的对象
- delete pOrig;
- return *this;
- }
这个例子看起来还是比较简单的,但是用处还是很大的,对于赋值操作符来说,很多情况都是需要重载的.
C++的异常处理机制是用于将运行时错误检测和错误处理功能分离的一 种机制(符合高内聚低耦合的软件工程设计要求), 这里主要总结一下C++异常处理的基础知识, 包括基本的如何引发异常(使用throw)和捕获异常(try catch)相关使用注意点, 以及C++标准库提供的一套标准异常类和这些异常类的继承层级结构以及相关使用方法和常用习惯.
C++异常的引发(throw):
引发C++异常的语法就是使用throw语句: throw object; 注意这里throw抛出的是一个对象,也就是说是一个实例. 一旦抛出, 发生两件事情: 第一, C++异常机制开始寻找try catch模块, 寻找和抛出的对象的类型相匹配的catch子句找到处理代码进行异常的处理, 这个过程是一个栈展开的 过程,也就是说C++讲先从当前的函数体里面寻找try catch模块, 如果没有, 则在调用当前函数(比如我们叫当前函数A)的函数(我们叫调用A的函数B)寻找处理代码(在B里面寻找), 一直寻找直到找到匹配的catch子句, 然后运行catch里面的代码, 运行完毕以后, 从这个匹配的catch后面的代码继续运行. 第二件事情是, 栈展开前面的所有函数作用域都失效(比如, A调用B, B调用C, C调用D, D调用E, E抛出异常同时在C找到了处理异常的catch子句, 那么D, E作用域失效, 等效于D, E运行到了函数结尾), 局部对象(自动释放内存的对象, 而不是那些动态分配内存的对象, 这一点和异常安全有关我们后面会提到)都将调用析构函数进行销毁.
注意点:
1. throw抛出的对象一定要是可以复制的(C++ Primer中的原话是: 异常对象是通过复制被抛出表达式的结果创建, 该结果必须是可以复制的类型)
2. 不要抛出(throw)一个数组或者函数, 原因是, 和函数参数一样, 数组和函数类型实参, 该实参自动转换为一个指针.
3. C++异常说明: void func(int) throw(exception type list), 表明函数func会且仅会抛出list中列举的异常对象类型, throw()表示不会抛出任何异常(空异常类型列表)
C++异常的捕获(try catch):
如果要试图捕获C++异常, 那么将可能抛出(throw)异常的代码块放到try{}里面, 在try{} 后面跟上catch(exception e) {}, 这里的e是一般的异常对象, C++异常处理通过抛出对象的类型来判断决定激活哪个catch处理代码. 具体语法可以参见任何一本C++的书籍. 这里主要提几点注意点:
1. 讲throw的时候也提到了, catch是一层一层catch(栈展开), 当寻找到main里面也没有catch捕获的时候, C++机制一般将调用terminate终止进程(abort)
2. catch子句列表中, 最特殊的catch必须最先出现, 不然永远都不可能执行到
3. catch(…) 这个语法表示catch捕获所有异常
4. 在catch里面使用throw ;这条语句将重新抛出异常对象, 改异常对象是和捕获的一场对象同一个对象(catch中可以修改这个对象)
C++标准异常介绍(继承层次结构等):
C++标准库提供了以下的标准异常类, 他们的继承层次结构如下(参考: Chapter 17: Advanced C++ Topics III).比较好的写异常的做法是继承这些C++标准的异常类, 然后定义一组适合自己应用的异常处理对象集合.

C++的异常处理机制主要用于将错误检测和错误处理功能分离, 从而达到低耦合的要求, 这篇文章主要总结了一下C++异常处理的基础知识, 从如何使用throw引发异常, 使用try catch等捕获异常到C++标准库提供的一套标准异常类和这些异常类的继承层级结构, 主要给出了相关使用方法和注意点以及一些程序设计的良好习惯. 文章全凭本人自己的理解原创行文, 如有不当之处, 在所难免, 还请不吝指正.
异常安全(内存泄露, 空指针等问题)
前言:
C++异常安全是针对C++异常处理带来的可能的隐患(内存泄露, 空指针等)而言的, 我们知道异常一旦发生, 程序就会转移控制权, 如果在转移控制权的之前, 没有妥善处理, 比如忘记释放内存, 空指针等, 会造成严重的未定义行为或者资源泄露(内存泄露, 空指针等). 所谓异常安全, 就是为了保证即使是发生了异常, 这些类似的未定义(内存泄露, 空指针等)行为也不会发生.
C++异常安全概念:
我们写程序的时候往往习惯按照假设程序正常运行的行为写代码, 管理资源等. 有时候也会写错误检测和处理的代码, 但是在这两个地方重叠时候, 也就是错误发生的时候的资源管理往往是容易被忽视的(下面马上会给出两个例子, 内存泄露问题和空指针未定义行为问题).
异常安全是这么一个概念: 这个是指, 即使发生异常, 程序也能正确操作(异常发生以后要杜绝一切未定义的行为, 包括空指针, 内存泄露等, 即使异常发生, 那么相关实例还是应该保持有效的状态).
C++异常安全要求:
C++异常安全一般有四个等级的要求(异常安全等级由低到高): 1. 没有任何异常安全保证, 也就是异常一旦发生, 可能造成程序行为的未定义; 2. 基本保证, 也就是异常发生的时候, 程序的行为还是合法的, 状态也都是有效的, 行为是有定义的, 但是程序实例的状态有可能改变(仍旧合法) 3. 强保证(回滚保证), 这个等级就要求异常一旦发生然后进行处理了以后, 要么一次性全部成功, 要么就回滚到异常钱的原始状态(程序状态和异常发生以前一模一样). 4. 保证不会有任何一方的发生.
这里面1是最不安全的, 不可取. 4基本上等级最强, 但是一般情况下不可能满足. 所以异常安全往往在2和3这两个等级间取舍. 等级3有可能会有额外的负担, 资源消耗等. 具体情况根据程序逻辑和实际情况判断取舍.
C++异常安全举例, 避免内存泄露:
C++异常安全其中一条重要的惯例, 是需要保证 如果发生异常, 被分配到的任何资源都适当地得到释放. 这个情况一般发生在动态分配内存的时候, 比如我程序里面有一段代码, 在第20行的时候首先动态分配了内存给一个指针p, 正常运行的话, 中间有一些处理代码, 然后到第40行delete [] p 释放内存, 程序正常运行的话没有问题, 但是要是在第20行到40行之间的代码出现了异常, 程序控制权转移给上级调用程序的时候, 这样的代码就有问题了, 此时, 作用域等效于已经到达了当前函数的结束, 所有局部变量或者实力都会调用自身的析构函数进行释放资源, 但是对动态分配内存的实例来讲, 因为是直接异常跳转, 虽然作用域结束, 但是没有执行到delete进行手动释放, 这块动态内存将造成内存泄露.
那么比较好的保证这一类内存资源不泄露的异常安全的技术成为“资源分配即初始化”(参考RAII). 对于这句话“资源分配即初始化”我自己是这么理解的, 我们要进行资源分配, 保证异常安全的做法不是普通的动态分配一块内存, 而是等效的初始化一个资源管理类的实例. 这就是所谓的“资源分配即初始化”, 也就是把资源分配等效的用初始化资源管理类来替代. 那么这里又提到了资源管理类, 我们解释一下资源管理类以及“资源分配即初始化”到底好处在哪里. 基本上这点要求我们设计一个资源管理类统一的管理资源的分配和释放, 更具体的, 利用构造函数分配资源, 利用析构行数释放资源. 这样做的好处呢, 是资源管理类本身是一个自动的局部对象, 不管是因为异常发生还是正常的程序运行到了改局部对象的作用域的结束的时候, 这个类的析构函数都会被调用从而保证了资源的释放, 避免了内存泄露问题. C++里面提供了RAII的auto_ptr类, 就是一个资源管理类, 行为雷系指针. 我们这里就不深入研究它了.
C++异常安全举例, 避免空指针:
C++异常安全的另一个常见的管理就是需要避免空指针. 这个情况的发生往往是我们在动态分配内存的时候发生了异常. 比如我们要分配p = new int[100], 这个时候要是内存不够, 那么就发生bad_alloc异常, p指针是空的NULL. 这个时候如果后面的代码依赖于p的未定义行为, 这样很容易导致程序的崩溃. 一个有效的避免空指针的做法就是, 在赋值之前就知道内存的分配是成功还是失败, 同样可以利用我们的资源管理类. 管理动态分配的内存, 如果分配成功, 那么将内存块的指针赋值给p, 如果失败, 那么抛出异常, 程序在p赋值前转移了控制权,此时p的值是不会改变的. 这样做就使得程序更加鲁棒(异常发生的时候, p的状态没有改变, 也没有产生未定义行为).
错误处理(返回值, 错误标志变量, 异常)
前言:
程序设计里面至关重要的一块就是错误处理, C++异常处理是一种面向对象的机制, 期望将错误处理和错误检测分离. 这里我们结合其他两种错误处理方式(返回值, 错误标志变量)来分析一下不同的错误处理(包括返回值判断, 错误标志变量, 异常处理机制)各有什么优缺点以及各自的适用环境.
函数返回值判断错误处理:
这种错误处理和判断的方法基本上是使用一组错误处理的常量, 然后通过函数返回值, 把错误信息返回给函数调用者. 比如如下简单的代码:
const int invalidPara = -2;
const int outOfRange = -3;
const int other = -4;
int func(int para)
{
if(invalid parameter)
return invalidPara;
do something here;
if(out of range)
return outOfRange;
if(other error)
return other;
}
这样的返回值判断的好处在于和系统API统一, 我们知道WinAPI以及Linux下面的系统函数都是以返回0(零)表示程序正常运行, 返回非零值表示不同的错误. 所以如果我们也采用这样的返回值判断的话可以和系统调用统一起来.
但是返回值判断错误的限制以及缺点也是很明显的(个人不是很推崇用返回值, 但是也还是要看具体情况). 首先呢, 返回值判断错误会破坏正常的返回值的作用, 使得函数调用不能被充分利用, 函数返回值不能作为其他表达式的组成部分, 因为这个返回值已经用来指示错误了而不是用来返回其他正常的计算结果, 即使可以既用于正常值计算又用于返回错误, 比如正常值都是正数, 错误值都是负数, 那这个结果还是不能直接被用作任何计算, 首先还是要判断这个是正常计算结果呢还是一个错误信息, 这就造成了计算的不方便.
其次很多时候其实是没办法使用返回值来判断错误信息的. 比如 1) 当func()返回类型是int的时候, 而且正常的结果的返回就是所有int型的值都有可能, 这个时候我们其实没法找到一个很好的int value 作为indicatro来指示这是个错误返回还不是一个正常的结果. 2) 编写范型的时候比如return T, 那怎么利用返回值来判断? 这个时候因为我们不明确T的类型, 所以也没有很好的办法利用一个明确的返回值来判断或者给出错误信息. 在这些情况下, 异常处理应该是更为合理的错误处理的方式. 我们后面第三条会再讲到。接下来可以看看第二种错误处理机制.
错误标志变量判断:
这个类型的错误判断基本上可以用下面的这段程序表示. 也就是设置一个错误标志变量, 然后通过引用或者指针的形式传递给被调用的函数, 函数一旦发现错误就设置这个标志, 上层调用者通过检查这个标志变量来判断是否有错误发生.
int funcCallee(int para, int &errorFlag) { if(invalid parameter) set errorFlag and return; do something here; if(out of range) set errorFlag and return; if(other error) set errorFlag and return; } int funcCaller(int para) { int errorFlag = 0; int ret = funcCallee(1, errorFlag); check errorFlag; }
这个方法的好处在于现在我们的返回值值表示正常计算结果, 可以被方便的利用起来, 比起第一种利用返回值判断的话是一个比较明显的优势, 而且前面提到的两种不能使用返回值判断错误的情况(泛型, 正常结果返回涵盖所有整型), 我们也可以使用标志位. 因为表示为总是可以保证是int型的, 而且是不受函数的代码逻辑影响的, 基本上是一个独立的错误标志. 在我看来这种方法似乎并没有明显的缺陷. 我个人比较推重.
C++异常处理机制:
其实我觉得C++的异常处理就是我们这里说的第二种利用标志变量的面向对象版本的错误处理机制, 本质上似乎没有太大区别. 当然异常处理还有复杂的多精细的多. 两者都是统一的独立于程序业务逻辑的错误处理机制. 比如不管程序干什么(泛型也好, 其他什么也好), 我们遇到错误总是能够抛出一个异常, 终止当前函数, 把控制权转移给上层调用函数进行处理. 对应到我们的第二种错误标志变量的话, 就是检测到异常或者错误的时候, 正确设置标识变量, 然后return, 控制权也转移给上层调用函数, 上层调用函数通过判断标志变量的值来进行处理. 从这个角度来讲, 似乎两者也没有太大区别.
另一方面呢, 异常机制作为C++的一种语言级别的机制, 其实会有比较大的开销, 包括控制权的转移等等, 他的好处在于错误处理和错误逻辑分离的很清楚, 而且强制使用者一定要处理异常, 否则程序将最终终止. 但是方法二呢, 要是我忘记去检查那个错误标志变量了怎么办? 回答是不怎么办. 因为这个仅仅是代码级别的判断, 没有任何强制措施去要求一定要处理。 这个就是很危险的了. 所以异常机制(语言级别的判断)从这个角度来讲也是比较好的一种错误处理的选择.
结束语:
这篇文章我们还是解析C++的异常处理机制, 这里我们结合其他两种错误处理方式(返回值, 错误标志变量)分析了这些不同的错误处理, 即返回值判断, 错误标志变量, 异常处理机制各有什么优缺点以及各自的适用环境.
posted on 2016-10-18 18:36 hackenzheng 阅读(257) 评论(0) 收藏 举报

浙公网安备 33010602011771号