- 9.1简介
程序的错误可以分为编译错误和运行错误两种。编译错误即语法错误,程序无法生存运行代码。运行时发生的错误可分为不可预料的逻辑错误和可以预料的运行异常。
逻辑错误由于程序设计不当造成。这类错误往往只有用户做了某些特定的错误才会出现,平时错误不可见的,所以很难查找。
运行异常可以预料但不能避免。它有系统运行环境造成的。这些错误使程序变得不稳定。但可以预料,加入一些预防代码就可以防止这些异常。
例:处理除“0”错误:
#include<iostream>
using namespace std;
int main()
{
int i,j;
cin>>i>>j;
if(j==0)
{
cout<<"divided by zero ";
extit(1);
}
cout<< i/j<<endl;
return 0;
}
上述方法是处理处理程序错误的方法,缺点是程序逻辑结构不清晰,用户不知道如何检查和处理各种异常。C++提出来异常的概念。异常处理的机制是在传统技术不冲分、不完整和容易出错的情况下,提供的一种替代技术,这种技术可以把错误代码从“正常”代码中分离出来,使程序的可读性增强,而且一定程度上保证了程序的健壮性。
- 9.2基本语法
C++使用try、throw和catch三个语句实现异常处理。程序中函数异常时使用throw函数将异常抛出,被抛给函数的调用者,调用者用catch捕捉抛出的异常并进行相应的处理。当预测到某段程序可能出现问题时,将该段程序放在try语句之后使用以使该段程序能够抛出异常。
异常处理语法:
(1)throw表达式语法
throw <表达式>
(2)try-catch表达式语法
try
{
受保护的程序段
}
catch(<异常类型声明>)
{
异常处理
}
catch(<异常类型声明>)
{
异常处理
}
......
说明:
(1)catch中的异常类型与throw抛出的异常类型匹配。当某个异常发生后,catch语句将将顺序执行,直到某个catch中的异常类型与throw抛出的类型相匹配,然后执行catch后面的异常处理。
(2)若出现catch(…),则该catch可以捕捉所以的异常类型。由于catch语句顺序执行,所以该语句应放置与所有其他catch语句的后面,否则该句后面的catch语句没有执行的可能。
(3)当某个异常被捕捉并被响应的程序段处理后,系统将继续执行捕捉函数的其余部分。产生异常的函数和捕捉异常的函数之间所有被调用的函数信息将被从调用栈清除(系统不会再执行这些函数)。
(4)如果一个异常函数没有被任何一个调用函数捕捉,系统函数terminate()会被调用,该函数的默认功能是调用abort()函数终止程序的运行。
例9.1抛出异常和捕获异常在同一个函数中
#include<iostream>
using namespace std;
int main()
{
int age;
char end='n';
while((end!='y')&&(end!='Y'))
{
try
{
cout <<"请输入一个年龄:";
cin>>age;
if(age<=0) throw "年龄不为负,输入错误";
cout<<"输入年龄的是:"<<age<<endl;
}
catch(char *p)
{
cout<<p<<endl;
}
cout<<"输入完毕(y/n)?"
cin>>end;
}
return 0;
}
程序输入结果:
请输入一个年龄:4
输入年龄的是: 4
输入完毕(y/n)?n
请输入一个年龄:-1
年龄不为负,输入错误
输入完毕(y/n)? y
异常的抛出和捕捉都是在main()函数中。实际开发中,异常的发生和处理通常不在同一个函数中,这样个各司其职,底层函数可以专注与解决具体的问题,上层调用函数可以在适当的位置针对不同类型的异常进行处理,而不用考虑异常是如何产生的,更符合程序开发的标准。
例9.1抛出异常和捕获异常在不在同一个函数中
#include<iostream>
#include<string>
using namespace std;
class Student
{
public:
Student (string name ,int age, unsigned long id ,char sex)
private
string name;
int age;
unsigned long id;
char sex;
};
Student :: Student(string name ,int age, unsigned long id ,char sex)
{
this->name=name;
this->sex=sex;
this->age=age;
if((this->name).length==0) throw "名字不为空";
if((this->age<10)||(this->age>40)) throw age;
if((this->sex!='M') &&(this->sex!='m')&&(this->sex!='w') &&(this->sex!='W')throw sex;
}
void Check()
{
try
{
cout<<"测试第一组数据"<<endl;
Student("zhang",11,1234567L,'m');
cout<<"测试第二组数据"<<endl;
Student("wang",9,1234467L,'w');
cout<<"测试第三组数据"<<endl;
Student("zhang",11,1224567L,'m');
cout<<"测试第四组数据"<<endl;
Student("",11,1134567L,'m');
}
catch(char)
{
cout<<"输入性别错误"<<endl;
}
catch(int)
{
cout<<"输入年龄不在合理范围之内";
}
catch(string)
{
cout<<"名字不能为空"<<endl;
}
}
int main()
{
cout<<"开始检查前"<<endl;
Check();
cout<<"检查结束"<<endl;
cout<<"回到main()函数";
return 0;
}
程序的运行结果
开始检查前
测试第一个数据
测试第二数据
输入年龄不在合理范围内检查结束
回到main函数
说明:(1)在构造函数中进行数据有效性检查,输入数据不符合会抛出异常。
(2)在保护程序段调用的构造函数,发现错误通过throw抛出异常,然后转到catch中顺序寻找与之对应类型的程序块。
(3)在产生异常的函数和捕捉异常的函数之间的所有被调用函数的信息将被从调用栈清除,系统不会在执行check()和Student的构造函数。所有不会出现“测试第三个数据”和“测试第四个数据”。
- 9.3构造函数、析构函数和异常
异常发生时,系统沿着函数调用链相反的方向搜索对异常的处理程序。找到对应的处理程序后,控制转到异常处理程序中去,而不会回到异常发生的位置。
控制转移过程中,所有调用链上被跳过的函数的运行环境被删除。在这个过程中,如果要释放对象,同样要调用析构函数
例9.2
#include<iostream>
#include<string>
#include<exception>
using namespace std;
class Message
{
public:
Message(string s);
~Message();
private:
string mes;
};
Message::Message(string s)
{
mes=s;
}
~Message::Message()
{
cout<<"destructor:"<<mes<<endl;
}
void A();
void B() throw (exception);
void C() throw (exception);
int main()
{
A();
return 0;
}
void A()
{
Message mes1("run away from A()");
try
{
B();
}
catch(exception& e)
{
cout<<"exception is caught :"<<e.what()<<endl;
}
}
void B()throw (exception)
{
Message mes2("run away from B()");
C();
}
void C()throw (exception)
{
Message mes2("run away from C()");
cout<<"throw an exception"<<endl;
throw exception("this is an exception");
}
程序运行结果:
throw an exception
destructor:run away from C()
destructor:run away from B()
exception is caught :this is an exception
destructor:run away from A()
说明:(1)程序中,函数C()引发了一个异常,该异常在A()中被处理。
(2) 异常发生后函数C()和函数B()的运行环境按调用链相反的顺序被删除,在其中建立的对象也一同被释放,通过析构函数完成。
例9.3构造函数异常
#include<iostream>
#include<string>
#include<exception>
using namespace std;
class Message
{
public:
Message(string s)throw (exception);
~Message();
private:
string mess;
};
Message::Message(string s) throw(exception)
{
mes=s;
throw exception ("exception in constructor");
}
Message::~Message()
{
cout<<"destructor:"<<mess<<endl;
}
void Fun() throw (exception);
int main()
{
try
{
Fun();
}
catch(exception e)
{
cout<<"caught the exception is:"<<e.what()<<endl;
}
return 0;
}
void Fun() throw (exception)
{
Message a("run away from Fun()");
}
程序运行结果:
caught the exception is:exception in constructor
说明:构造函数发生异常时,因为对象并未构造好,此时的对象被删除并不调用对象的析构函数。
- 9.4继承和异常
各种不同的异常可以用公共基类派生出来,使用类的层次机构后,catch处理器捕获到一个基类对象的指针或引用,同样可以捕获到公有派生类的所有对象的指针和引用,就可以利用多态来处理相关的错误。
异常继承来捕获错误有两种,其中一种是分别捕获异常派生类的各种引用或指针,另一种是捕获基类的指针或引用,第二种更简洁,且两种与多态面临的情况一样。 - 9.5 标准库中的异常类
C++标准库定义了一组类,用于报告在标准库中的函数遇到的问题。编程时可以使用这些标准的异常类。标准库异常类定义在如下四个文件中。
exception: 定义了最常见的异常类。
stdexcept:定义了几种常见的异常类,见下表。
new:定义了bad_alloc异常类。
type_info:定义了bad_cast异常类。
stdexcpet中定义的标准异常类
异常类 | 描述 |
---|---|
Exception | 常见问题 |
runtime_error | 运行时错误 |
range_error | 结果无意义 |
overflow_error | 上溢 |
underflow_error | 下溢 |
logic_error | 逻辑错误 |
domain_error | 结果值不存在 |
invalid_argument | 非法参数 |
length_error | 超过长度限制 |
out_of_range | 超出有效范围 |
标准库中异常类之间的继承关系如图
说明:(1)标准异常类提供的操作有限,包括创建、赋值和赋值异常类对象。
(2)exception、bad_alloc以及bad_cast自定义了默认构造函数,无法创建这些类型的对象时提供初值。
(3)其余的异常类型都只定义了一个有string参数的构造函数,对象创建者需要提供一个string类型的实参初始化对象,并通过该参数为所发生的错误提供更多信息。
例9.3构造函数的异常
#include<iostream>
#include<exception>
using namespace std;
int main()
{
try
{
range_error Rerror("range_error");
throw(Rerror);
}
catch(const exception & eError)
{
cout<<eError.what()<<endl;
}
try
{
length_error Lerror("length_error");
throw(Lerror);
}
catch(const exception & eError)
{
cout<<eError.what()<<endl;
}
return 0;
}
程序的运行结果:
range_error
length_error