C++ 异常

一、异常的概述

  1. 程序有时会遇到运行阶段错误,导致程序无法正常的运行下去。
  2. 例如:程序可能试图打开一个不可用的文件,请求过多的内存,或者遭到不能容忍的值。
  3. 通常,程序员都会试图预防这种意外情况。所以c++出现了异常的功能。

1.基本方法

讨论异常之前,了解可使用的一些基本方法,例:计算两个的调和平均数的函数为例。两个数的调和平均数的定义是:这两个数字倒数的平均值的倒数。表达式:2.0xy/(x+y)
如果y是x的负值,则上述公式将导致被零除:这是不允许的运算。对于被零的情况,编译器将生成一个表示无穷大的特殊浮点值,cout将这种值显示为Inf,inf,INF或类似的东西。程序也可能会崩溃。

2.调用abort()

  1. 对于上面的情况,处理方法之一,若参数是另一个参数的负值,调用abort()函数。
  2. 函数头文件cstdlib(stdlib.h),实现是向标准错误流(即cerr使用的错误流)发送信息: 程序异常终止,返回随实现而异的值,处理失败。
  3. abotr()是否刷新文件缓冲区取决于实现。也可以使用exit(),该函数刷新缓冲区,不显示信息。

实例:

#include <iostream>
#include<cstdlib>
using namespace std;
double hmean(double a,double b);
int main()
{
    double x,y,z;
    cout<<"Enter two numbers:";
    while(cin>>x>>y)
    {
        z=hmean(x,y);
        cout<<"Harmonic mean of"<<x<<"and"<<y
           <<"is"<<z<<end;
        cout<<"Enter next set of number <q to quit>:";
    }
    cout<<"Bye!\n";
    return 0;
}
double hmean(double a, double b)
{
    if(a==-b)
    {
        cout<<"unenable arguments to hmean()\n";
        abort();//调用将直接终止程序,不是返回到main()
    }
    return 2.0*a*b/(a+b);
}

在这里插入图片描述

为了避免异常终止,程序应在调用hmean()函数之前检查x和y的值。

二、返回错误码

  1. 一种比异常终止更灵活的方法:使用函数的返回值来指出问题。
  2. 如:ostream类的get(void)成员返回下一个输入字符的ASCII码,但到达文件尾时,将返回特殊值EOF。
  3. 对于hmean()不管用,任何数值都是有效的返回值,因此不存在可用于指出问题的特殊值。
  4. 这时,可以使用指针参数或引用参数来将值返回给调用程序,并使用函数的返回值来指出成功还是失败。
  5. istream族重载>>运算符使用了这种技术的变体。使得程序采取除异常终止程序之外的其他措施。

例:将hmean()的返回值重新定义为bool,让返回值指出成功了还是失败。例:

#include <iostream>
#include<cfloat>
using namespace std;
bool hmean(double a,double b, double * ans);
int main()
{
    double x,y,z;
    cout<<"Enter two numbers:";
    while(cin>>x>>y)
    {
        if(hmean(x,y,&z))
        cout<<"Harmonic mean of"<<x<<"and"<<y
           <<"is"<<z<<endl;
        else
            cout<<"one value should not be the negative"
               <<"of theother-try again'\n";
        cout<<"Enter next set of numbers <q to quit>:";
    }
    cout<<"Bye!\n";
    return 0;
}
bool hmean(double a, double b,double * ans)
{
    if(a==-b)
    {
        *ans=DBL_MAX;
        return false;
    }
    else
    {
    return 2.0*a*b/(a+b);
    return true;
    }
}

在这里插入图片描述

三、异常机制

异常的组成部分:

  1. 引发异常;
  2. 使用处理程序捕获异常;
  3. 使用try块;

程序在出现问题时将引发异常。

例如:可以修改hmean(),而不是调用abort()函数,throw语句实际上是跳转,即命令程序跳到另一条语句。

throw关键字表示引发异常,紧随后的值(例如字符串或对象)指出了异常的特征。

  1. 程序使用异常处理程序来捕获异常,异常处理程序位于要处理问题的程序中,catch关键字表示捕获异常。
  2. 处理程序以关键字catch开头,随后是位于括号中的类型声明,它指出了异常处理程序要响应的异常类型;
  3. 花括号括起的代码块,指出要采取的措施。
  4. catch关键字和异常类型用作标签,指出当异常被引发时,程序应调到这个位置执行。异常处理程序也被称catch块。
  5. try标识其中特定的异常可能被激活的代码块,try的后面是一个由花括号括起来的代码块,表明需要注意这些代码引发的异常。

在这里插入图片描述
例:

#include <iostream>
using namespace std;
double hmean(double a,double b);
int main()
{
    double x,y,z;
    cout<<"Enter two numbers:";
    while(cin>>x>>y)
    {
    	//捕获开始
        try{
            z=hmean(x,y);
        }//捕获结束
        catch(const char * s)//异常处理开始
        {
            cout<<s<<endl;
            cout<<"Enter a new pair of numbers:";
            continue;
        }//异常处理结束
        cout<<"Harmonic mean of"<<x<<"and"<<y
           <<"is"<<z<<endl;
        cout<<"Enter next set of numbers <q to quit>:";
    }
    cout<<"Bye!\n";
    return 0;
}

double hmean(double a, double b)
{
    if(a==-b)
        throw "bad hmean() arguments:a=-b not allowed";//抛出异常,throw将程序控制权返回给main()。
        return 2.0*a*b/(a+b);
}

在这里插入图片描述

  1. 1-10传递给函数,if语句导致hmean()引发异常。这将终止hmean()的执行。

  2. hmean()函数是从main()中的try块中调用的,因此查找与异常类型匹配的catch块。

  3. 唯一的一个catch块的参数为char*,因此它与引发异常匹配。程序将字符串"bad hmean() arguments:a=-b not allowed"赋给变量s,然后执行处理程序的代码。

在这里插入图片描述

  1. 其中被引发的异常是字符串bad hmean() arguments:a=-b not allowed。异常类型可以是字符串或其他c++类型;通常为类类型。

  2. 如果函数引发异常,而没有try块或没有匹配的处理程序时,在默认·的·情况下。程序最终会调用abort()函数。但可以修改这种行为。

四、将对象用作异常类型
  1. 引发异常的函数将传递一个对象,优点之一是:使不同的异常类型来区分不同的函数在不同情况下引发的异常。

  2. 对象可以携带信息,方便确定引发异常的原因。

  3. catch块可以根据这些信息来决定采取什么措施。

例:下面是针对函数hmean()引发的异常而提供的一种设计:

class bad_hmean
{
private:
double v1;
double v2;
public:
bad_hmean(int a=0,int b=0):v1(a),v2(b){}
void mesg();
};
inline void bad_hmean::mesg()
{
cout<<"hmean("<<v1<<","<<v2<<"):"
<<"invalid arguments:a=-b/n";
}

可以将一个bad_hmean对象初始化为传递给函数hmean的值,而方法mesg()可用于报告问题(包括传递给函数hmean()的值)。函数hmean()可使用下面的代码:

if(a==-b)
throw bad_hmean(a,b);

上述代码调用构造函数bad_hmean(),以初始化对象,使其存储参数值。

下面的两个程序添加了另一个异常类 bad_gmean以及另一个名为gmean()的函数,该函数引发bad_gmean异常。

该函数gmean计算两个数的几何平均值,即乘积的平方根,要求两个参数都不为负,为负则引发异常。

头文件:

#ifndef EXC_MEAN_H
#define EXC_MEAN_H
#include<iostream>
using namespace std;
class bad_hmean
{
private:
    double v1;
    double v2;
public:
    bad_hmean(double a=0,double b=0):v1(a),v2(b){}
    void mesg();
};

inline void bad_hmean::mesg()

{
    cout<<"hmean("<<v1<<", "<<v2<<"):"
       <<"invalid arguments:a=-b\n";
}
class bad_gmean
{
public:
    double v1;
    double v2;
    bad_gmean(double a=0,double b=0):v1(a),v2(b){}
    const char*mesg();
};

inline const char * bad_gmean::mesg()
{
    return "gmean() arguments should be >=0\n";
}

#endif // EXC_MEAN_H

源文件:

#include <iostream>
#include<cmath>
#include"exc_mean.h"
using namespace std;
double hmean(double a,double b);
double gmean(double a,double b);
int main()
{
    double x,y,z;
    cout<<"Enter two numbers:";
    while(cin>>x>>y)
    {
        try{
            z=hmean(x,y);
            cout<<"Harmonic mean of"<<x<<"and"<<y
               <<"is"<<z<<endl;
            cout<<"Geometric mean of"<<x<<"and"<<y
               <<"is"<<gmean(x,y)<<endl;
            cout<<"Enter next set of numbers <q to quit>:";
        }
        catch(bad_hmean & bg)//bad_hmean异常捕获
        {
            bg.mesg();
            cout<<"Try again.\n";
            continue;
        }
        catch (bad_gmean &hg)//bad_gmean异常捕获
        {
            cout<<hg.mesg();
            cout<<"Values used:"<<hg.v1<<","
               <<hg.v2<<endl;
            cout<<"Sorry,you don't get to play any more.\n";
            break;
        }
    }
    cout<<"Bye!\n";
    return 0;
}

double hmean(double a, double b)
{
    if(a==-b)
    {
        throw bad_hmean(a,b);
        return 2.0*a*b/(a+b);
    }
}

double gmean(double a, double b)
{
    if(a<0 || b<0)
        throw bad_gmean(a,b);
    return sqrt(a*b);
}

在这里插入图片描述

在这里插入图片描述
bad_hmean异常处理程序使用continue,bad_gmean 异常处理程序使用break。给函数hmean() 提供的参数不正确,跳出,进入下一次。给函数gmean()提供参数不正确将结束循环。

五、异常规范和c++11

一种理念看似有前途,但实际的使用效果并不好。一个这样的例子是异常规范,这是c++98新增的一项功能,但c++11却抛弃了。意味着c++11仍然处于标准之中,但以后可能从标准中剔除,因此不建议使用它。
忽视异常规范前,应该知道它是什么样的:

double harm(double a)throw(bad_thing);//可能会抛出异常
double marm(double) throw();//不会抛出异常

异常规范的作用:

  1. 告诉用户可能需要使用try块。
  2. 让编译器添加执行运行阶段检查的代码,检查是否违反了异常规范。很难检查。

c++建议忽略异常规范。然而,c++11却支持一种特殊的异常规范:可使用新增的关键字noexcept指出函数不会引发异常:

double marm()noexcept;//marm()不会抛出异常
六、栈解退

异常被抛出后,从进入try块起,到异常被抛掷前,这期间在栈上构造的所有对象,都会被 自动析构。析构的顺序与构造的顺序相反,这一过程称为栈的解旋。

实例:

//头文件:
#ifndef EXC_MEAN_H
#define EXC_MEAN_H
#include<iostream>
using namespace std;
class bad_hmean
{
private:
    double v1;
    double v2;
public:
    bad_hmean(double a=0,double b=0):v1(a),v2(b){}
    void mesg();
};

inline void bad_hmean::mesg()

{
    cout<<"hmean("<<v1<<", "<<v2<<"):"
       <<"invalid arguments:a=-b\n";
}
class bad_gmean
{
public:
    double v1;
    double v2;
    bad_gmean(double a=0,double b=0):v1(a),v2(b){}
    const char*mesg();
};

inline const char * bad_gmean::mesg()
{
    return "gmean() arguments should be >=0\n";
}

#endif // EXC_MEAN_H
//源文件:
#include<cmath>
#include<string>
#include"exc_mean.h"
using namespace std;
class demo
{
private:
    string word;
public:
    demo(const string & str)
    {
        word=str;
        cout<<"deom"<<word<<"created\n";
    }
    ~demo()
    {
        cout<<"deom"<<word<<"destroyed\n";
    }
    void show()const
    {
        cout<<"deom"<<word<<"lives!\n";
    }
};
double hmean(double a,double b);
double gmean(double a,double b);
double means(double a,double b);

int main()
{
    double x,y,z;
    {
        demo d1("found in block in main()");
        cout<<"Enter two numbers:";
        while(cin>>x>>y)
        {
            try{
                z=means(x,y);
                cout<<"The mean mean of"<<x<<"and"<<y
                   <<"is"<<z<<endl;
                cout<<"Enter next pair:";
            }
            catch(bad_hmean &bg)
            {
                bg.mesg();
                cout<<"Try again.\n";
                continue;
            }
            catch(bad_gmean &hg)
            {
                cout<<hg.mesg();
                cout<<"Values used:"<<hg.v1<<". "
                   <<hg.v2<<endl;
                cout<<"Sorry,you don't get to paly any more.\n";
                break;
            }
            d1.show();
        }
        cout<<"Bye!\n";
        cin.get();
        cin.get();
        return 0;
    }
}

   double hmean(double a,double b)
   {
        if(a==-b)
            throw bad_hmean(a,b);
        return 2.0*a*b/(a+b);
        sqrt(a*b);
    }
   double gmean(double a, double b)
   {
       if(a<0||b<0)
           throw bad_gmean(a,b);
       return sqrt(a*b);
   }
    double means(double a, double b)
    {
        double am,hm,gm;
        demo d2("found in means()");
        am=(a+b)/2.0;
        try
        {
            hm=hmean(a,b);
            gm=gmean(a,b);
        }
        catch(bad_hmean &bg)
        {
            bg.mesg();
            cout<<"Caught in means()\n";
            throw;
        }
       d2.show();
       return (am+hm+gm)/3.0;
    }

在这里插入图片描述
程序说明:

  1. demo类的构造函数指出,在main()函数中创建一个deom的对象,调用函数means(),它创建了另一个demo对象。

  2. 函数means()使用6和2调用函数hmean()和gmean(),将结果返回means(),计算一个结果将其返回。

  3. 返回结果前,means()调用了d2.show();返回后,函数means()指向完毕。

因此自动调用析构函数:

demo found in means() lives;
demo found in means() destroyed

输入循环将值6,-6发给means(),然后means()创建一个新的deom对象,并将值传递给hmean()函数,hmean()引发bad_hmean异常,该异常被means()中的catch块捕获,下面输出指出:

hmean(6,-6):invalid arguments:a=-b
Caught in means()

该catch块中的throw语句导致函数means()终于执行,并将异常传递给main()函数。语句d2.show()没有被执行表明means()函数被提前终止。但需要指出的是,还是为d2调用析构函数:

deom found in means() destroyed

程序进行栈解退以后到能够捕获异常的地方时,将释放栈中的自动存储型变量。如果变量是类对象,将为该对象调用析构函数。

第三次输入循环:

  1. 6,-8被发给函数means(),means()创建一个新的demo对象,然后将6,-8传递给hmean(),后者处理没问题。

  2. means()将6和-8传递给gmean(),后者引发bad_gmean异常。

  3. 由于means()不能捕获bad_gmean异常,不能指行means()的其代码。

程序进行栈解退时,释放局部的动态变量,因此为d2调用了析构函数:

demo found in means() destroyed

最后,bad_gmean异常处理程序捕获了该异常,循环结束终止,显示信息自动为d1调用析构函数。

如果catch块使用的是exit而不是break,程序将立刻终止。

七、其他异常特性

虽然throw-catch机制类似于函数参数和函数返回机制,但还有不同之处:

  1. 函数fun()的返回语句将控制权返回到调用fun()的函数,但throw语句将控制权向上返回到第一个这样的函数:包含能够捕获相应异常的try-catch组合。
  2. 如上面程序,函数hmeans()引发异常时,控制权传递给函数means();当gmean()引发异常时,控制权传递到main()。
  3. 引发异常时编译器总是创建一个临时拷贝,即使异常规范和catch块中指定的是引用。例:
class problem{...};
...
void super()throw(problem)
{
	...
	if(oh_no)
	{
	problem oops;
	throw oops;
	...
	}
	...
	try{
	super();
	}
	catch(problem & p)
	{
	}
//p将指向oops的副本而不是oops本身。

  1. 既然throw语句将生成副本,为何代码中使用引用呢?因为,引用还有个重要特征:基类引用可以执行派生类对象。
  2. 假设有继承关联的异常类型,在异常规范中需列出基类引用,它将与任何派生类对象匹配。
  3. 假设有异常类层次结构,并分别处理不同的异常类型,则使用基类引用将能够捕获任何异常对象,而使用派生类对象只能捕获它所属类和派生来的类的对象。

引发的异常对象被第一个与之匹配的catch块匹配,意味着catch块的排列顺序应该与派生顺序相反:

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

  1. bad_1&处理程序放在最前面,它将捕获异常bad_1,bad_2和bad_3;通过按相反的顺序排列,bad_3异常将被bad_3&处理程序所捕获。
  2. 若有一个异常类继承层次结构,应这样排列catch块:将捕获位于层次结构最下面的异常类的catch语句放在最前面,将捕获·基类异常的catch语句放在最后面。
  3. 有时不知道会发生那些异常。例如:编写一个调用另一个函数的函数,却不知道调用的函数可能引发那些异常。但下面的方法仍能捕获异常,即使不知道异常的类型。

方法:使用省略号来表示异常类型,从而捕获任何异常:

catch(...){}

若知道一些可能会引发的异常,可以将上述捕获所有异常的catch块放在最后面,有点类似于switch语句中的default:

try{
duper();
}
catch(bad_3 &be)
{}
catch(bad_2 &be)
{}
catch(bad_1 &be)
{}
catch(bad_hmean &h)
{}
catch(...)
{}

可以创建捕获对象而不是引用的处理程序。在语句中使用基类对象时,将捕获所有的派生类对象,但派生特性将被剥去,因此将使用虚方法的基类版本。

八、exception类

  1. 较新的c++编译器将异常合并到语言中,例如:支持该语言,exception 头文件定义了exception类,c++可以把它用作其他异常类的基类。

  2. 代码可以引发exception异常,也可以将exception类用作基类。

若有个名为what()的虚拟成员函数,它返回一个字符串,该字符串的特征随实现而异。然而,由于这是个虚方法,因此可以在从exception派生而来的类中重新定义它:

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

如果不想以不同的方式处理这些派生而来的异常,可以在同一个基类处理程序中捕获它们:

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

否则,可以分别捕获它们。c++库定义了很多基于exception的异常类型。

九、stdexcept异常类

头文件stdexcept定义了其他几个异常类。首先,该文件定义了logic_errorruntime_error类,它们都是以公有方式从exception派生而来:

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

注:这些类的构造函数接受一个string对象作为参数,该参数提供了方法what()以c-风格字符串方式返回的字符数据。

这两个新类被用作两派生类系列的基类。异常类系列logic_error描述了典型的逻辑错误。每个类的名称指出了它用于报告的错误类型:

  1. domain_error:让函数在参数不在定义域-1到1之间时引发异常
  2. invalid_argument:指出给函数传递了一个意料外的值。引发异常
  3. length_error:用于指出没有足够的空间来执行所需的操作。引发异常
  4. out_of_bounds:用于指出索引错误。

每个类独有一个类似于logic_error的构造函数,让你能够提供一个方法what()返回的字符串。

runtime_error异常系列描述了可能在运行期间发生但难以预计和防范的错误。每个类的名称指出了它用于报告的错误类型:

  1. range_error;
  2. overflow_error;
  3. underflow_error;

下溢错误在浮点数中,存在浮点类型可以表示的最小非零值,结果比这个值还小时将导致下溢错误。

整型和浮点型都可能发生上溢错误,当计算结果超过了某种类型能够表示的最大数量级时,将发生上溢错误。

结果不在函数允许的范围之内,但没有发生上溢或下溢错误,可以使用range_error错误。

每个类独有一个类似与runtime_error的构造函数,让您能够提供一个供方法what()返回的字符串。

总结:

  1. logic_error系列异常表明存在可以通过编程修复的问题,而runtime_error系列异常表明存在无法避免的问题。
  2. 但是这些错误类有相同的常规特征,主要区别:1.不同的类名让您能够分别处理每种异常。2.继承关系让您能够一起处理它们。

例:下面的代码首先单独捕获out_of_bounds异常,然后统一捕获其他logic_error系列异常,最后统一捕获exception异常,runtime_error系列异常以及其他从exception派生而来的异常:

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

注:如果上述库类不能满足您的需求,应该从logic_error或runtime_error派生一个异常类,以确保异常类可归入同一个继承层次结构中。

十、bad_alloc和new

  1. 对于使用new导致的内存分配问题,c++的最新处理方式是让new引发bad_alloc异常。
  2. 头文件new包含bad_alloc类的声明,它是从exception类公有派生而来的。但在以前,当无法分配请求的内存的内存量时,new返回一个空指针。

例:捕获异常显示继承的what()方法返回的消息,然后终止。

#include <iostream>
#include<new>
#include<cstdlib>
using namespace std;

struct Big
{
    double stuff[20000];
};
int main()
{
    Big * pb;
    try{
        cout<<"Trying to get big block of memory:\n";
        pb=new Big[10000];
        cout<<"Got past the new request:\n";
    }
    catch(bad_alloc &ba)
    {
        cout<<"Caught the exception!\n";
        cout<<ba.what()<<endl;//返回字符串
        exit(EXIT_FAILURE);
    }
    cout<<"Memory successfulluy allocated\n";
    pb[0].stuff[0]=4;
    cout<<pb[0].stuff[0]<<endl;
    delete [] pb;
    return 0;
}

在这里插入图片描述
如果程序在您的系统上运行时没有出现内存分配问题,可尝试提供请求的内存量。

十一、空指针和new

很多代码都是在new在失败时返回空指针时编写的。

为处理new的变化,有些编译器提供了一个标记,让用户选择所需的行为。

当前,c++标准提供了一种在失败时返回空指针的new,其用法如下:

int * pi=new (nothrow)int;
int *pa=new (nowthrow)int [500];

使用这种new,可将上面的核心代码改为:

Big * pb;
pb=new (nothrow)Big[10000];
if(pb==0)
{
cout<<"Could not allocate memory Bye.\n";
exit(EXIT_FAILURE);
}
  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值