6:异常
6.1 为什么需要异常?
- 异常机制的处理原理
程序会出现错误,尤其是不易察觉的错误。需要了解并解决这些错误。通常,程序出现错误,都会强制退出,很难排除错误原因。
6.2 C语言如何表示错误
1、函数返回值
- 通常,成功返回0,返回值-1。
- 返回值为指针类型,成功返回非NULL,失败返回值NULL。
例如:malloc();例外shmat()失败返回值为MAP_INVALD(-1)
- 其它另类的返回值
fread()/fwrite()
返回读写字符长度size_t
,超出长度表示失败。
2、全局变量errno
6.3 异常处理特点
异常提供一个错误专用通道。
6.4 语法
异常分为两个部分:抛出异常与捕获并处理异常。
throw 表达式;
try {
// 保护代码 包含可能抛出异常的语句;
} catch (类型名 [形参名]) {
// catch块 处理异常
}
- 特点
1、只要抛出异常,异常后的代码不再执行。
2、异常的所抛出与经过的栈都会销毁。
3、异常捕获具有类型匹配,只有相同的或者父类类型才能匹配到。
4、如果多个catch都能接受相同异常,只有最前面的一个可以接收到。catch(...)
只能放在所有异常捕获的最后
多个catch
表达式:
try {
// 保护代码 包含可能抛出异常的语句;
} catch (类型名1 [形参名]) {
// catch块 处理异常
} catch (类型名2 [形参名]) {
// catch块 处理异常
} catch (类型名3 [形参名]) {
// catch块 处理异常
} catch(...){
// catch块 处理异常
}
实例:多个catch
#include <iostream>
#include <cmath>
using namespace std;
//三角形
class Triangle{
protected: //允许子类访问,不允许对象访问
int a,b,c;
public:
Triangle():a(0),b(0),c(0){} //初始化成员变量
Triangle(int a,int b,int c):a(a),b(b),c(c){
if(a <= 0 || b <= 0 || c <= 0)throw invalid_argument("三角形三边不能为0或负数");
if(!Check()) throw invalid_argument("三边不能构成三角形");
} //构造函数
int GetLength() const { //const修饰this指针
return a+b+c;
}
virtual float GetArea(){
float q = GetLength()/2.0;
return sqrt(q*(q-a)*(q-b)*(q-c));
}
bool Check()const{
return a+b > c && a+c > b && b+c > a;
}
};
//直角三角形
class RightAngleTriangle:public virtual Triangle{
public:
bool ChechRightAngleTriangle(){
return a*a+b*b == c*c;
}
RightAngleTriangle(int a,int b,int c):Triangle(a,b,c){
if(!ChechRightAngleTriangle()) throw invalid_argument("三边不能构成直角三角形");
}
float GetArea()const{
throw runtime_error("error");
return a*b/2.0;
}
};
int main(){
int a,b,c;
cin >> a >> b >> c;
try{
RightAngleTriangle rat(a,b,c);
cout << rat.GetLength() << "," << rat.GetArea() << endl;
}catch(invalid_argument& e){
cout << e.what() << endl;
}catch(exception& e){
cout << "exception" << e.what() << endl;
}catch(...){ //其
-1 2 3
三角形三边不能为0或负数
1 2 3
三边不能构成三角形
5 6 7
三边不能构成直角三角形
测试用例3,4,5求完周长之后,抛出异常,不再求面积
3 4 5
12,exception:error
注:异常机制try-throw-catch
的目标是问题检测与问题解决分离
实例:实现除法
#include <iostream>
using namespace std;
int main() {
int n,m;
cin >> n >> m;
//if(0 == m) throw "除数不能为0";
try {
if(0 == m) throw runtime_error("除数不能为0");
cout << n/m << endl;
} catch(exception& e) {
cout << e.what() << endl;
}
}
2 0
除数不能为0
6.5 异常的接口声明/异常规范
返回值类型 函数() throw(异常列表);
指定函数可以抛出何种异常,如果没有throw(异常列表)
默认可以抛出所有异常。
指定函数不抛出函数,异常列表为空throw()
。
#include <iostream>
using namespace std;
void test(){
cout << "before throw." << endl;
throw -1;
cout << "after throw." << endl;
}
int main(){
try{
test();
}catch(int a){
cout << "exception:" << a << endl;
}
}
before throw.
exception:-1
实例2:异常与局部对象析构
#include <iostream>
using namespace std;
class Test{
public:
Test(){
cout << "Test Init" <<endl;
}
~Test(){
cout << "Test Destroy" <<endl;
}
};
int main(){
try{
Test t;
cout << "before throw." << endl;
throw -1;
cout << "after throw." << endl;
}catch(int a){
cout << "exception:" << a << endl;
}
}
Test Init
before throw.
Test Destroy
exception:-1
注:
1、如果抛出的异常一直没有函数捕获(catch),则会一直上传到c++运行系统那里,导致整个程序的终止。
2、一般在异常抛出后资源可以正常被释放,但注意如果在类的构造函数中抛出异常,系统是不会调用它的析构函数的,处理方法是:如果在构造函数中要抛出异常,则在抛出前要记得删除申请的资源。
3、异常处理仅仅通过类型而不是通过值来(switch-case)匹配的,所以catch块的参数可以没有参数名称,只需要参数类型。
4、函数原型中的异常说明要与实现中的异常说明一致,否则容易引起异常冲突。
5、应该在throw语句后写上异常对象时,throw先通过Copy构造函数构造一个新对象,再把该新对象传递给 catch.
6、catch块的参数推荐采用地址传递而不是值传递,不仅可以提高效率,还可以利用对象的多态性。另外,派生类的异常捕获要放到父类异常捕获的前面,否则,派生类的异常无法被扑获。
7、编写异常说明时,要确保派生类成员函数的异常说明和基类成员函数的异常说明一致,即派生类改写的虚函数的异常说明至少要和对应的基类虚函数的异常说明相同,甚至更加严格,更特殊。
6.6 自定义异常类
- 编码流程
1.继承异常类exception
2.实现接口what()
- 代码结构
class 异常类:public exception {
public:
const char* what()const throw() {
return 信息字符串;
}
};
#include <iostream>
using namespace std;
// 自定异常类
class SimpleException:public exception{
string msg;
public:
SimpleException(const string& msg):msg(msg){}
const char* what() const throw(){
return msg.c_str();
}
};
class Simple{
public:
Simple(){
cout << __func__ << endl;
throw SimpleException("Simple::Simple() Error");//runtime_error("construction error");
}
~Simple(){cout << __func__ << endl;}
void Test(){
throw runtime_error("error");
}
};
int main(){
try{
Simple s;
s.Test();
}catch(exception& e){
cout << e.what() << endl;
}
}
Simple
Simple::Simple() Error
- 构造函数、析构函数的异常处理
1、构造函数可以抛出异常,此时不会调用析构函数,所以如果抛出异常前,申请了资源,需要自己释放。
2、C++标准指明析构函数不能、也不应该抛出异常。
3、C++标准规定,构造函数失败,析构函数不会执行。就是说在构造函数抛出异常前分配的资源将无法释放。
4、如果析构函数抛出异常,则异常点之后的程序不会执行,如果析构函数在异常点之后执行了某些必要的动作比如释放某些资源,则这些动作不会执行,会造成诸如资源泄漏的问题。
4、通常异常发生时,c++的机制会调用已经构造对象的析构函数来释放资源,此时若析构函数本身也抛出异常,则前一个异常尚未处理,又有新的异常,会造成程序崩溃的问题。
6.7 是否使用异常机制
为什么很多经典书籍鼓励使用异常,但是实际开发中很多C++编码规范却禁用异常?
C++异常机制在语法上是更加优雅的处理错误,但是实际上编译出来的程序会有一些性能损失,另外错误地使用异常处理代码会变得更加复杂。