从C到C++___异常

异常

异常是指在程序运行阶段的错误,例如被除数为0。在C语言中,我们处理异常的方法是:调用abort()或者exit()函数,或者采用返回错误码。C++提供了更好的方法处理异常.

1.传统C方法

1.1 调用abort()函数

abort()exit()函数在头文件cstdlib中,它门两会向标准错误流发送信息,然后终止程序。

#include<iostream>
#include<cstdlib>
double foo(double a,double b);
int main()
{
    using namespace std;
    double a,b;
    cout<<"Enter two values: ";
    while(cin>>a>>b)
    {
        double result=foo(a,b);
        cout<<"the result: "<<result<<endl;
        cout<<"Enter two values: ";
    }
}
double foo(double a,double b)
{
    if(b==0)
    {
        std::cout<<"Error! b=0 not allowed!\n";
        std::abort();
    }
    return a/b;
}

如果b=0的话,程序会直接终止,而不会返回main函数。而我们希望的是,即使运行出现异常,程序也不会终止。

1.2 返回错误码

#include<iostream>
#include<cstdlib>
bool foo(double a,double b,double & result);
int main()
{
    using namespace std;
    double a,b;
    cout<<"Enter two values: ";
    while(cin>>a>>b)
    {
        double result;
        if(foo(a,b,result))
        {
            cout<<"the result: "<<result<<endl;
        }
        cout<<"Enter two values: ";
    }
}
bool foo(double a,double b,double & result)
{
    if(b==0)
    {
        std::cout<<"Error! b=0 not allowed!\n";
        result=0;
        return false;
    }
    result=a/b;
    return true;
}

稍微对上面那个程序做了修改,使得即使出现异常,也会返回main函数。

2. C++ throw-catch机制

2.1 try块的使用

C++异常机制有3个组成部分:

  • 引发异常
  • 使用处理程序捕获异常
  • 使用try

下面介绍3个关键词

  • throw

这个相当于return,它会引发异常,但是不会终止程序。而且throwreturn一样会返回信息,一般是字符串或者对象。

  • catch

这是用来捕获异常的,throw返回的信息,会被catch捕获。

  • try

try块,就是在代码块前面加个try。一般try块里面就是可能引发异常的语句,在try块后面会跟一个或者多个catch来捕获它的异常信息。

#include<iostream>
double foo(double a,double b);
int main()
{
    using namespace std;
    double a,b;
    cout<<"Enter two values: ";
    while(cin>>a>>b)
    {
        double result;
        try{
            result=foo(a,b);    
        }
        catch(const char* s)
        {
            cout<<s<<endl;
            cout<<"Enter two values: ";
            continue;
        }
        cout<<"the result: "<<result<<endl;
        cout<<"Enter two values: ";
    }
}
double foo(double a,double b)
{
    if(b==0)
        throw "Error! b=0 not allowed!";
    return a/b;
}

try块中如果某个语句引发异常,就会被紧随其后的catch捕获到,并执行。
throw关键词和return特别像,它终止函数的执行,但是 throw不是把控制器交给调用程序,而是沿函数调用序列后退,直到找到包含try块的函数
catch块类似于函数定义,但他不是函数定义,他是一个异常处理程序,而char* s是表明该处理程序和字符串异常相匹配。异常引发字符串被赋给s,然后执行catch块代码。

  • noexcept关键词

这是一种特殊的异常规范,它出现在函数原型或者定义中,例如:void foo() noexcept{...} 它告诉编译器,这个函数保证不会出现异常。

2.2 将对象用作异常类型

我们推荐将对象用作异常类型,因为对象里面包含的信息很多,而且类具有很多很好的性质。

看看下面这段代码:

#include<iostream>
#include<cmath>
double average(double a,double b);
double foo(double a,double b);
double fun(double a,double b,double c);
class Bad_foo
{
private:
    double a;
    double b;
public:
    Bad_foo(double x1=0,double x2=0):a(x1),b(x2){}
    void mesg() const
    {
        std::cout<<"foo("<<a<<","<<b<<") error!\n";
        std::cout<<"foo(a,b) arguments: b=0 not allowed!\n";
    }
};
class Bad_ave
{
private:
    double a;
    double b;
public:
    Bad_ave(double x1=0,double x2=0):a(x1),b(x2){}
    void mesg() const
    {
        std::cout<<"average("<<a<<","<<b<<") error!\n";
        std::cout<<"sqrt(double a) arguments: a<0 not allowed!\n";
    }
};

int main()
{
    using namespace std;
    double a,b,c;
    cout<<"Enter three values: ";
    while(cin>>a>>b>>c)
    {
        double result1,result2,result3;
        try{
            result1=foo(a,b);//b!=0
            cout<<"result1: "<<result1<<endl;
            result2=average(a,b);//a*b>=0    
            cout<<"result2: "<<result2<<endl;
            result3=fun(a,b,c);//b*c>0
            cout<<"result3: "<<result3<<endl;
            cout<<"Enter three values: ";
        }
        catch(Bad_foo & bad)
        {
            bad.mesg();
            cout<<"Enter three values: ";
            continue;
        }
        catch(Bad_ave & bad)
        {
            bad.mesg();
            cout<<"Enter three values: ";
            continue;
        }
    }
}
double foo(double a,double b)
{
    if(b==0)
        throw Bad_foo(a,b);
    return a/b;
}
double average(double a,double b)
{
    using std::sqrt;
    if(a*b<0)
        throw Bad_ave(a,b);
    return sqrt(a*b);
}
double fun(double a,double b,double c)
{
    double ret=foo(a,average(b,c));
    return ret;
}
Enter three values: 1 2 3
result1: 0.5
result2: 1.41421
result3: 0.408248
Enter three values:  1 0 3
foo(1,0) error!
foo(a,b) arguments: b=0 not allowed!
Enter three values: 1 -2 3
result1: -0.5
average(1,-2) error!
sqrt(double a) arguments: a<0 not allowed!
Enter three values: 1 2 -3
result1: 0.5
result2: 1.41421
average(2,-3) error!
sqrt(double a) arguments: a<0 not allowed!
Enter three values: 1 2 0
result1: 0.5
result2: 1.41421
foo(1,0) error!
foo(a,b) arguments: b=0 not allowed!
Enter three values: q

上面代码反映了两个事实:

  • try块中代码,如果出现异常,那么会立即将控制权转到块尾的异常处理程序,例如上例中,如果result1出错,那么后面就不会执行result2result3
  • 栈解退

在计算result3时,我们调用了函数fun(),而fun()中使用了可能出现异常的foo()average(),如果出现异常,那么程序控制权不会返回给调用函数,而是返回给main()函数中try块尾的异常处理情况。

假设函数由于出现异常而终止,而程序也会释放栈中的内存,但不会在释放栈的第一个返回地址(即调用函数地址)后停止,而是继续释放栈,直到找到一个位于try块中的返回地址,随后,控制权将转到块尾的异常处理程序,这个过程叫做栈解退

下面时returnthrow的区别:

2.3 其他特性

throw-catch机制类似于函数参数和函数返回机制,但是又两点不同,一点是栈解退,另一点是引发异常时编译器总是创建一个临时拷贝,即使异常规范和catch块中指定的是引用。

class problem{...};
void super()
{
    if(oh_no)
    {
        problem oops;
        throw oops;
    }
}
...
try{
    super();
}
catch(problem & p)
{
    ...
}

上面这段代码中,p指向的是oops的副本而不是本身。这样设计的原因是:在super()函数发生异常时,oops将不复存在(即被退栈了),此外,使用对象引用的好处是可以引用所有派生类对象。

例如:

class bad_1{...};
class bad_2:public bad_1{...};
class bad_3:public bad_2{...};
...
void duper()
{
    if(oh_no)
        throw bad_1();
    if(rats)
        throw bad_2();
    if(drat)
        throw bad_3();
}
...
try{
    duper();
}
catch(bad_3 &be){...}
catch(bad_2 &be){...}
catch(bad_1 &be){...}

如果将bad_1 &处理程序放到最前面,他将捕获bad_1,bad_2,bad_3;通过按相反的顺序排列,bad_3异常将被bad_3 &处理程序捕获。

总之,通过正确地排列catch块的顺序,让您可以在如何处理异常方面有选择的余地

有时,我们并不知道被调用的函数可能引发哪些异常,那么我们可以使用下面这样的catch程序:
catch(...){//statement},用...代替catch的参数列表。

这样子,我们可以类似于switch语句:

try{
    duper();
}
catch(bad_3 &be){/*statemnet*/}
catch(bad_2 &be){/*statement*/}
catch(bad_1 &be){/*statement*/}
catch(...)
{
    //statement
}

catch(...){//statement}类似于switch中的default语句。

3. exception

在头文件exception中定义了exception类,这个类就是处理异常的,C++把它作为其他异常类的基类。在我们自己写的代码中,我们可以引发exception异常.也可以将它作为基类。这个类中有一个what()的虚拟成员函数,它返回一个字符串。由于这是一个虚方法,因此在派生类中我们可以重新定义它。

#include<exception>
class Bad_hmean:public std::exception
{
public:
    const char* what(){return "bad arguments to hmean()";}
    ...
};
class Bad_gmean:public std::exception
{
public:
    const char* what(){return "bad arguments to gmean()";}
    ...
};

我们也可以直接使用现成的exception类来处理异常:

try{
    ...
}
catch(std::exception & e)
{
    cout<<e.what()<<endl;
}

3.1 stdexcept异常类

头文件stdexcept中定义了logic_errorruntime_error类,它们都是从exception类中公有派生出来的

class logic_error :public exception
{
    public:
        explicit logic_error(const string& what_arg);
        ...
};

注意,这些派生类都接受一个string对象作为参数,该参数提供了方法what()以C风格字符串方式返回的字符串

logic_errorruntime_error类又被做成两个派生类系列的基类。

logic_error异常类系列:描述了典型的逻辑错误,逻辑错误可以通过合理编程来避免。
这些类是:

  • domain_error 将参数传给函数时,参数不在其定义域中,这样会引发定义域异常
  • invalid_argument传给函数一个意料之外的值,会引发无效参数异常
  • length_error 没有足够空间来执行所需操作,会引发长度异常,例如string::append()方法合并字符串时长度超过最大允许长度时
  • out_of_bounds 索引错误,例如operator[]中使用的索引无效。

runtime_error异常类系列:描述了可能在运行期间发生但难以预计和防范的错误。
这些类是:

  • overflow_error 上溢错误
  • underflow_error 下溢错误
  • range_error 结果不在允许范围内,但是没有溢出的错误

总之,目前我们介绍了C++自带的7个异常类,和3个基类。
你可以这样使用它们:

try{
    ...
}
catch(out_of_bounds & oe)
{...}
catch(logic_error & oe)
{...}
catch(exception &oe)
{...}

上面这段代码,首先捕获out_of_bounds异常,然后捕获其他logic_error异常,最后统一捕获exception及其派生类的一切异常.

如果你觉得这些异常,并不能满足需求,你可以从logic_error或者runtime_error派生出一个异常类。

3.2 bad_alloc异常和new

头文件new中包含一个bad_alloc异常类的声明,这个类是从exception类公有派生类的。bad_alloc在无法分配请求的内存时,会引发该异常。

例如:

#include<iostream>
#include<new>

struct Big
{
    double stuff[2000000];
};

int main()
{
    using std::cout;
    using std::endl;
    using std::bad_alloc;
    Big *pb;
    try{
        cout<<"Trying to get a big block of memory:\n";
        pb=new Big[10000];
    }
    catch(bad_alloc& ba)
    {
        cout<<"Caught the exception!\n";
        cout<<ba.what()<<endl;
    }
    delete [] pb;

}
Trying to get a big block of memory:
Caught the exception!
std::bad_alloc

在这里,方法what()返回字符串std::bad_alloc

  • 空指针和new

很多代码都是在new失败时,返回空指针时编写的。为处理这种情形,C++标准提供了一种在失败时返回空指针的new,其用法如下:

pb = new(std::nothrow) Big[10000];
if(pb==0)
{
    cout<<"can't allocate memory.\n";
}

3.3 异常、类和继承

//异常6.h
#include<stdexcept>
#include<string>

class Sales
{
public:
    enum{MONTHS=12};
private:
    double gross[MONTHS];
    int year;
public:
    class bad_index:public std::logic_error
    {
    private:
        int bi;//bad index value
    public:
        explicit bad_index(int ix,const std::string &s="Index error in Sales object\n")
                :std::logic_error(s),bi(ix){}
        int bi_val() const{return bi;}
        virtual ~bad_index(){}
    };
    explicit Sales(int yy=0):year(yy)
    {
        for(int i=0;i<MONTHS;i++)
            gross[i]=0;
    }
    Sales(int yy,const double *gr,int n)
    {
        year=yy;
        int lim=(n<MONTHS)?n:MONTHS;
        int i;
        for(i=0;i<lim;i++)
            gross[i]=gr[i];
        for(;i<MONTHS;i++)
            gross[i]=0;
    }
    virtual ~Sales(){}
    int Year() const {return year;}
    virtual double operator[](int i) const;
    virtual double & operator[](int i);
};
class LabeledSales:public Sales
{
private:
    std::string label;
public:
    class nbad_index:public Sales::bad_index
    {
    private:
        std::string lbl;
    public:
        nbad_index(const std::string & lb,int ix,
             const std::string &s="Index error in LabeledSales object\n")
             :Sales::bad_index(ix,s),lbl(lb){}
        const std::string & label_val() const{return lbl;}
        virtual ~nbad_index(){}
    };
    explicit LabeledSales(const std::string &lb="none",int yy=0):Sales(yy),label(lb){}
    LabeledSales(const std::string &lb,int yy,const double *gr,int n):Sales(yy,gr,n),label(lb){}
    virtual ~LabeledSales(){}
    const std::string & Label() const{return label;}
    virtual double operator[](int i) const;
    virtual double & operator[](int i);
};
double Sales::operator[](int i) const
{
    if(i<0||i>=MONTHS)
        throw bad_index(i);
    return gross[i];
}
double & Sales::operator[](int i)
{
    if(i<0||i>=MONTHS)
        throw bad_index(i);
    return gross[i]; 
}
double LabeledSales::operator[](int i) const
{
    if(i<0||i>=MONTHS)
        throw nbad_index(label,i);
    return Sales::operator[](i);
}
double& LabeledSales::operator[](int i)
{
    if(i<0||i>=MONTHS)
        throw nbad_index(label,i);
    return Sales::operator[](i);
}
//异常6.cpp
#include<iostream>
#include"异常6.h"

int main()
{
    using std::cout;
    using std::cin;
    using std::endl;
    double vals1[12]=
    {
        1220,1100,1122,2212,1232,2344,
        2884,2393,3302,2922,3002,3544
    };
    double vals2[12]=
    {
        12,11,22,21,32,34,
        28,29,33,29,32,35
    };
    Sales sales1(2011,vals1,12);
    LabeledSales sales2("Blogstar",2012,vals2,12);

    cout<<"First try block:\n";
    try
    {
        int i;
        cout<<"Year = "<<sales1.Year()<<endl;
        for(i=0;i<12;i++)
        {
            cout<<sales1[i]<<" ";
            if(i%6==5)
                cout<<endl;
        }
        cout<<"Year = "<<sales2.Year()<<endl;
        cout<<"Label = "<<sales2.Label()<<endl;
        for(i=0;i<12;i++)
        {
            cout<<sales2[i]<<" ";
            if(i%6==5)
                cout<<endl;
        }
        cout<<"End of try block 1.\n";
    }
    catch(LabeledSales::nbad_index &bad)
    {
        cout<<bad.what();
        cout<<"Company: "<<bad.label_val()<<endl;
        cout<<"bad index: "<<bad.bi_val()<<endl;
    }
    catch(Sales::bad_index &bad)
    {
        cout<<bad.what();
        cout<<"bad index: "<<bad.bi_val()<<endl;
    }
    cout<<"\nNext try block:\n";
    try
    {
        sales2[2]=37.5;
        sales1[20]=23345;
        cout<<"End of try block 2.\n";
    }
    catch(LabeledSales::nbad_index &bad)
    {
        cout<<bad.what();
        cout<<"Company: "<<bad.label_val()<<endl;
        cout<<"bad index: "<<bad.bi_val()<<endl;
    }
    catch(Sales::bad_index &bad)
    {
        cout<<bad.what();
        cout<<"bad index: "<<bad.bi_val()<<endl;
    }
    cout<<"done\n";
}
First try block:
Year = 2011
1220 1100 1122 2212 1232 2344 
2884 2393 3302 2922 3002 3544 
Year = 2012
Label = Blogstar
12 11 22 21 32 34 
28 29 33 29 32 35 
End of try block 1.

Next try block:
Index error in Sales object
bad index: 20
done

对上面程序的说明:

我们将异常类作为公有嵌套类放到我们设计的类中。
当从logic_error类中派生出自定义异常类时,我们不需要重新写what()方法,我们在写构造函数的时候,创建logic_error子对象时,会自动修改what()方法。

4. 未捕获异常和意外异常

4.1 未捕获异常

异常在引发后,可能没有try块或者没有匹配的catch块,导致异常无法被处理,这种异常叫做未捕获异常
在默认情况下,未捕获异常的出现会直接终止程序,但是我们可以修改这种行为。
默认情况下,未捕获异常的出现会调用terminate()函数,然后terminate()会调用abort()函数来终止程序。现在,我们可以使用set_terminate()函数来修改terminate()的调用函数。

set_terminate()的原型:

typedef void (*terminate_handler)();
terminate_handler set_terminate(terminate_handler f) noexcept;
void terminate() noexcept;

typedefterminate_handler变成函数指针的别名,然后set_terminate()函数的返回值和参数都是函数地址(函数指针)。并且set_terminate()terminate()函数都是不允许出现异常的。

set_terminate()terminate()都是在头文件exception中声明的。

#include<cstdlib>
#include<iostream>
#include<exception>
using namespace std;
void myQuit()
{
    cout<<"Terminating due to uncaught exception\n";
    exit(5);
}
int main()
{
    set_terminate(myQuit);
    try
    {
        throw exception();
    }
    catch(double a){}
}

上面这段代码就会引发未捕获异常,但是我们修改了terminate()的调用函数,如果未捕获异常出现,terminate()函数就会调用myQuit()函数处理未捕获的异常。

4.2 意外异常

C++98采用了异常规范,目前这一功能受到很多人诟病,但是我们仍然允许使用异常规范
我们不推荐使用规范异常机制,因此这部分知识可以不看。
异常规范是指:我们可以在函数定义或者原型中,规定这个函数只可能出现哪些异常。

void foo1() throw();
void foo2() throw(logic_error,const char*);
void foo3() noexcept;

上面这个foo1foo3的原型指出函数中不允许出现异常,foo2的原型指出,函数中只允许出现logic_errorconst char*异常.

意外异常是指在存在异常规范的情况下,规范中不容许的异常
注意:noexcept关键字不属于规范异常,throw()规范的函数中出现的异常是意外异常,而noexcept修饰的函数中出现的异常是未捕获异常
如果发生意外异常,那么程序会调用unexpected(),然后unexcepted()会调用terminate(),后者在默认情况下,调用abort()来终止程序。我们可以使用set_unexpected()函数来修改unexpected()中的调用函数,也可以使用set_terminate()函数来修改terminate()的调用函数。

typedef void (*unexpected_handler)();
unexpected_handler set_unexpected(unexpected_handler f) noexcept;
void unexpected() noexcept;

意外异常比未捕获异常复杂且危险,这也是我们抛弃异常规范的原因。

相较于使用set_terminate(),使用set_unexpected()会复杂得多:
我们通常提供给set_unexpected()一个函数地址,这个函数会引发异常。

  • 如果新引发的异常和异常规范匹配,我们想象一下,当意外异常出现,程序调用unexpected()函数,然后unexpected()函数引发一个新的异常,而这个异常和异常规范匹配,那么就会正常进行异常处理。总之,**使用预期异常取代意外异常。
  • 如果新引发的异常和异常规范不匹配,且异常规范里面没有std::bad_exception类型,那么就会调用terminate()函数,(std::bad_exception是从exception中公有派生的,专门处理意外异常的类)
  • 如果新引发的异常和异常规范不匹配,但是异常规范里面有std::bad_exception类型,则不匹配的异常会被std::bad_exception类型接受,然后正常进行异常处理。

总之,你如果想要自己处理意外异常,你就在使用规范异常的时候,在容许异常类型中添加一个std::bad_exception类型,然后使用set_unexpexted()函数,给他传入一个函数地址,这个函数会引发异常,(随便异常都行,引发新异常注意是为了栈解退将程序返回给try块)

#include<exception>
#include<iostream>
using namespace std;

void myUnexpected()
{
    cout<<"myUnexpected()"<<endl;
    throw;
}
void foo() throw(const char*,bad_exception)
{
    int a=2;
    throw a;
}
int main()
{
    set_unexpected(myUnexpected);
    try
    {
        foo();
    }
    catch(const char* s){}
    catch(bad_exception &e)
    {
        cout<<"Caught in main()"<<endl;
    }
}
myUnexpected()
Caught in main()

上面那个foo()函数引发了意外异常,然后我们修改了unexpected()函数的行为,使之引发新异常,然后就被bad_exception类型接受,然后程序控制权返回在main函数中被try块,然后被catch到。

5.关于异常的注意事项

我们应该在设计程序的时候就加入异常处理功能,而不是以后再添加。
使用异常会增加程序代码,降低程序运行速度。
异常规范不适用于模板,因为模板函数引发的异常可能随着特定具体化而异
动态内存分配和异常。

void test1(int n)
{
    string mesg("I'm trapped in an endless loop");
    ...
    if(oh_no)
        throw exception();
    ...
    return;
}

string类采用动态内存分配.通常,当函数结束时,将为mesg调用string的析构函数。肃然throw语句过早的终止了函数,但他仍然使得析构函数被调用,这要归功于栈解退。因此上面这段代码时没有问题的。

void test2(int n)
{
    double *ar= new double[n];
    ...
    if(oh_no)
        throw exception();
    ...
    delete[] ar;
    return;
}

上面这段代码有问题,在栈解退的时候,将删除栈中的变量ar,但是由于程序过早引发异常,导致delete[]语句被忽略,这就导致内存泄漏(即指针消失,内存没被释放)。

这种泄漏是可以避免的:

void test2(int n)
{
    double *ar= new double[n];
    ...
    try
    {
        if(oh_no)
            throw exception();
    }
    catch(exception &e)
    {
        delete[] ar;
        throw;//throw后面不加参数的意思是,重新返回原来异常,即exception
    }
    ...
    delete[] ar;
    return;
}

实际上,异常处理是非常困难的事情,你必须了解库文档和源代码中的异常和错误的理解细节。

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值