C++查缺补漏之异常

1.什么是异常?

异常就是计算机程序在运行过程中发生的不正常的行为,比如计算机的内存耗尽,计算发生溢出,或者是其他有碍于计算机正常运行的情况都可以视为异常,为了让程序按照我们的意愿能够正常的运行,我们就需要进行异常处理。就像我们在疾驰的汽车上,前面突然出现了一块巨石,要是我们不想摔得头破血流,那么就要避让这个突然出现巨石。

2.异常处理

使用异常处理,程序中独立开发的各部分能够就程序执行期间出现的问题相互通信,并处理这些问题。程序的一个部分能够检测出本部分无法解决的问题,这个问题检测部分可以将问题传递给准备处理问题的其他部分。 我们可以将问题的检测部分和处理部分进行分离,而连通两者之间的纽带就是异常,通过异常在两者之间进行通信。
我们先看一个简单的例子:
#include <iostream>
using namespace std;

int main(int argc, char const *argv[])
{
    try{
        cout<<"Enter a number in [0 ,10]:"<<flush;
        int number ;
        cin >> number;
        if(number < 0 || number >10 )
            throw number;
        cout<<"Get the right number "<<number<<endl;
    }catch(const int number)
    {
        cerr<<"Get the wrong number "<<number<<endl;
    }
    return 0;
}
上面try以内的是问题的检测部分,catch则是问题的处理部分,而连通二者之间的就是那个int类型的异常变量,是不是很神奇

2.1抛出类类型的异常

异常是由throw引起的,抛出异常的类型决定激活那块处理代码。被选中的处理代码是调用链中与对象类型匹配且离抛出位置最近的那个
在执行了throw以后不会执行throw以后的代码,就像在函数里面直接return了一样。而是将代码的工作流转到了相应的问题处理代码块里面执行,这里有两个重要含义:
1.沿着调用链的函数提早退出
2.一般而言,在处理异常的时候,抛出异常的块中的局部存储就已经不存在了
就像在上面的代码中一样,try块在抛出异常以后,接下来的打印语句是不工作的,并且try块里面number局部变量将被释放,不知道你有没有注意到,因为try块里面的number已经被释放了,那么catch里面的number的值是从哪里来的呢?因为在try块里面的number被释放之前,就将number复制到了一个所有catch都可以访问的地方,然后遇到对应的处理代码时,就将之前复制的变量像函数传递参数一样,传入到catch块里面,那么这里就可知,被抛出的异常一定要是可以复制的类型!
下面我们来看一个异常类型不可复制的例子:
#include <iostream>
using namespace std;

class A
{
public:
    A(std::string err):errMsg(err){}
    std::string what() const{return errMsg;}
private:
    A(const A & a){}// define copy constructer
    A operator=(const A & a){}
    std::string errMsg;
};  

int main(int argc, char const *argv[])
{
    try{
        throw A(std::string("Hello World"));
    }catch(const A & err)
    {
        cout<<err.what()<<endl;
    }
    return 0;
}
上面的例子中,我们将复制构造函数定义为private的,那么编译得到下面错误:
C:\Users\Administrator\Desktop\exception.cc: In function 'int main(int, const char**)':
C:\Users\Administrator\Desktop\exception.cc:10:5: error: 'A::A(const A&)' is private
     A(const A & a){}// define copy constructer
     ^
C:\Users\Administrator\Desktop\exception.cc:18:43: error: within this context
         throw A(std::string("Hello World"));
                                           ^
[Finished in 1.6s with exit code 1]
要是将复制构造函数定义为public以后,编译通过!

2.1.1异常对象与继承

当抛出一个表达式的时候,被抛出对象的静态编译时的类型将决定异常对象的类型

2.1.2异常与指针

情况一:对指针进行解引用,然后抛出,抛出的异常类型与指针类型相匹配。如果抛出的指针指向的是派生类,那么就有可能抛出的类型(基类)与实际的类型(派生类)不同,即将派生类截断,只抛出基类部分
情况二:只抛出指针本身,在抛出的时候复制了指针本身,并没有复制指向的内存。如果指针指向的是try块的局部变量,那么在throw异常时候,内存被释放,但是指针还是指向原来的内存块,就好比在函数里面返回一个指向函数局部变量的指针,这样情况比较危险,如果真要抛出指针,最好确保指针指向的对象在catch里面还是可以被访问到的!一般不建议抛出一个指针。

2.2栈展开

抛出异常的时候,就暂停当前函数的执行,开始查找匹配的catch子句,首先检查throw本身是否在try块内部,如果是,检查与该try相关的catch子句,看其中时候有与异常类型匹配。如果找到catch,那么就处理异常,如果没有找到,就退出当前函数,释放当前内存并撤销局部内存,并继续在调用函数里面查找。在找到相应的catch处理代码后,就在这里catch之后继续执行,这个过程就是栈展开。

2.2.1为局部对象调用析构函数

我们知道在抛出异常的时候会提前退出包含throw的函数或者调用链上的其他函数。那么这些函数创建的局部变量会在函数退出时被撤销。每个函数退出的时候,它的局部存储都被释放,在释放内存之前,撤销异常发生之前创建所有的对象。
如果一个块直接分配资源,而在释放资源之前发生异常,那么在栈展开期间不会释放该内存,比如下面代码:
try{
int * ptr =new int(1024);
throw runtime_error("Hello World");
delete ptr;
}catch(runtime_error & err){}
那么这这里ptr指向的内存将不会被释放,要解决这个问题我们最好使用auto_ptr(后面会提到),这种智能指针,就算提前抛出异常,也可以释放内存!

2.2.2析构函数应该从不抛出异常

前面我们提到过,在抛出异常栈展开的时候,这些函数创建的局部变量会在函数退出的时候撤销,会调用对象的析构函数,但要是在析构函数里面抛出一个异常会怎么样呢?原来的异常还没有处理,又来了一个新的异常,原来的异常会被新的异常取代么?应该忽略掉析构函数里面的异常么?下面我们来在实验一下会发生什么
#include <iostream>
#include <stdexcept>

using namespace std;

class A
{
public:
    A(){}
    ~A()
    {
        throw runtime_error("throw second exception");
    }
};
int main(int argc, char const *argv[])
{
    try
    {
        throw A();
    }
    catch(const A & a)
    {
        cout<<"Get the exception"<<endl;
    }
    while(1);
    return 0;
}
编译运行后结果为:
Get the exception
terminate called after throwing an instance of 'std::runtime_error'
  what():  throw second exception

This application has requested the Runtime to terminate it in an unusual way.
Please contact the application's support team for more information.
接着程序崩溃了
我们看到,上面的catch接收到了异常,,并且这个异常是A类型的,并不是runtime_err类型的,但是紧接着就因为在析构函数里面抛出异常程序崩溃了
原因是在为某个异常进行栈展开的时候,析构函数如果又抛出自己的未经处理的另一个异常,将会导致调用标准库terminate函数,一般而言,terminate函数将调用abort函数,强制从整个程序非正常退出!
所以最好不要,应该是不该在析构函数里面抛出异常,一般析构函数是释放资源,也不会抛出异常。标准库类型都保证他们的析构函数不会抛出异常

2.2.3异常与构造函数

构造函数不像析构函数一样,构造函数一般会在内存中申请内存,进行一些初始化操作,较大可能会抛出异常,所以在构造函数里面抛出异常的时候,那么有一些成员已经初始化,另外一些成员没有初始化,即使对象只被部分构造了,也要保证将会适当的撤销已构造的成员

2.2.4未捕获的异常终止程序

不能不处理异常!如果抛出异常后找不到对应的catch,那么程序就会调用标准库函数terminate

2.3捕获异常

catch子句中的异常说明符看起来就像只包含一个形参的形参表,异常说明符是在其后跟一个形参名的类型名。 
异常说明符决定了异常处理的类型,异常类型必须是内置类型,或者是已经定义的类型,前置声明是不行的!catch只是为了了解异常类型的时候,就可以省略形参,要是想知道异常的具体附加信息,那么就可以加上形参

2.3.1查找匹配的处理代码

在查找匹配的catch期间,找到的catch不鄙视与异常最匹配的那个,而是离抛出点最近的与catch可以匹配的那个catch,所以需要将最特殊的异常说明符放在离异常抛出点最近的地方。
异常与异常说明符的匹配类似于形参与实参的匹配,但是异常与异常说明符的匹配较实参与形参的匹配更为严格,有些转换时不允许的,除了下面几种的转换,异常得类型需要与catch异常说明符必须完全匹配!
  • 非const类型转换为const类型
  • 从派生类向基类转换
  • 数组,函数向指针的转换(不存在数组和函数类型的异常)
我们来看一个例子:
#include <iostream>
using namespace std;

int main(int argc, char const *argv[])
{
    try{
        int err(-1);
        throw err;
    }catch(const float & /*err*/)
    {
        cout<<"catch the float exception"<<endl;   
    }catch(const int & /*err*/)
    {
        cout<<"catch the int exception"<<endl;
    }
    while(1);
    return 0;
}
上面的处理块是哪一个catch呢?
运行一下结果为:
"catch the int exception"
因为上面的int类型到float类型是不允许的

那要是下面这个例子呢:
#include <iostream>
using namespace std;

class A
{
public:
    A(){}
    virtual void what() const
    {
        cout<<"This is class A"<<endl;
    }
};

class B : public A
{
public:
    B(){}
    void what() const
    {
        cout<<"This is class B"<<endl;
    }
};

class C : public B
{
public:
    C(){}
    void what() const
    {
        cout<<"This is class C"<<endl;
    }
};

int main(int argc, char const *argv[])
{
    try
    {
        throw C();
    }catch(const A  a)
    {
        a.what();
    }catch(const B  b)
    {
        b.what();
    }catch(const C  c)
    {
        c.what();
    }
    while(1);
    return 0;
}
运行结果为:
“This is class A”
因为class C 是class A的派生类,所以在第一个catch的时候就被截住了

2.3.2异常说明符

进入catch的时候,用异常对象初始化catch的形参。就像函数形参一样,异常说明符可以是引用。异常对象本身是被抛出对象的副本,时候讲异常对象复制到catch位置取决于异常说明符的类型
如果异常说明符不是引用类型,那么就将异常复制到catch位置,对形参做的任何改变只作用于形参本身,对原本的异常对象没有任何影响。如果异常说明符是引用类型,那么对形参做出的任何更改就是作用于实际的异常对象
下面我们来看一个例子:
void testFunc()
{
    try{
        int a = 10;
        throw a;
    }catch(int & a)
    {
        a = 11;
        throw;
    }
}

int main(int argc, char const *argv[])
{
    try{
        testFunc();
    }catch(int & err)
    {
        cout<<"err is "<<err<<endl;
    }
    while(1);
    return 0;
}

这里结果是10还是11呢?
运行结果为:11
我们这里异常说明符是引用类型,在testFunc函数里面对a的修改起始就是对异常对象本身的修改,当再次抛出的时候与,异常对象已经被修改了

2.3.3异常说明符与继承

我们知道基类的异常说明符可以接受派生类的异常,如果基类类型是非引用的话,那么派生类传给基类的时候就会发生截断,只保留基类的对象,如果基类类型是引用的话,那么将会发生动态绑定,操作的异常对象本身,catch对象的静态类型可以与catch对象所引用的的异常对象的动态类型不同
如果将上面的main函数里面的内容改为这样呢:
int main(int argc, char const *argv[])
{
    try
    {
        throw C();
    }catch(const A &  a)
    {
        a.what();
    }catch(const B &  b)
    {
        b.what();
    }catch(const C &  c)
    {
        c.what();
    }
    while(1);
    return 0;
}
运行结果是什么呢?
“This is class C”
因为异常说明符类型是基类引用,可以绑定到派生类,在第一个catch就被截住了,这里动态特性就调用class C对象的成员函数。

未完待续。。。



  • 1
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值