【C++基础】自定义异常类与多重捕获

173 阅读6分钟

目录

自定义异常类

自定义异常类通常由exception或其后代类派生。这样我们就可以去override e.what() 函数。

构建过程

这里我们以构建一个Vec3D类的数组下标越界的异常类为例子。
在这里插入图片描述

自定义异常类过程:
1、找一个高级一点的类,继承一下
class RangeException : public out_of_range
2、定义一些能够记录异常问题的变量
size_t dimension;
int index;
3、由异常类来记录问题,所以要将变量放到异常类的构造函数中,
RangeException(size_t dimension, int index);
4、给父类加入提示信息,在异常类的构造函数的初始化列表上对父类的构造函数也进行初始化。
注意exception基类构造函数是不接受参数的,其派生类可以接受一个字符串参数,用来描述该异常
RangeException () : out_of_range(“Vec index error”) {};

例:Vec3D类的数组下标越界的异常类

task1:创建Vec3D类,用array保存向量成员
task2:创建RangeException类,定义构造函数
RangeException(std::size_t dimension,const int index)
task3:实现Vec3D::operator[](const int index)
当index越界时,抛出RangeException的对象
task4:在主函数创建Vec3D对象并调用[]制造越界问题,捕获异常并输出异常中的信息。

RangeException.h:

#pragma once

#include <iostream>
#include <exception>
class RangeException : public std::exception {
private:
	std::size_t dimension{ 3 };
	int index{ 0 };
public:
	RangeException (std::size_t dimension,const int index) {
		this->dimension = dimension;
		this->index = index;
	}
	std::size_t getDimension() {
		return dimension;
	}
	int getIndex() {
		return index;
	}
};

Vec3D.h:
这里需要注意一个点:数组下标运算符不加&时,返回的是一个右值,不能通过数组下标运算符修改数组的值。加上&后,返回的就是一个左值了。

#pragma once
#include <array>
#include "RangeException.h"
class Vec3D {
private:
	std::array <double, 3> v{1.0,1.0,1.0};
public:
	Vec3D() = default;
	Vec3D(double x, double y, double z){
		v[0] = x;
		v[1] = y;
		v[2] = z;
	}
	double &operator [] (const int index) {
		if(index >=0 && index <= 2) {
			return v[index];
		}
		else {
			throw RangeException(3,index);
		}
	}
	
};

当主函数为:

#include <iostream>
#include "Vec3D.h"
using namespace std;

int main()
{
	Vec3D v1 {1.2,1.3,1.4};
	cout << v1[4];
	return 0;
}

可以看见,抛出了我们自定义的异常。
在这里插入图片描述
接下来我们捕获这个异常:
注意,此时我们是捕获exception基类类型的异常,由于RangeException是从exception继承下来的,所以我们能够捕获,但是我们没有对exception的what进行覆写,所以不会打印异常信息。

#include <iostream>
#include <exception>
#include "Vec3D.h"
using namespace std;

int main()
{
	Vec3D v1 {1.2,1.3,1.4};
	try {
		cout << v1[4];
	}
	catch (exception & e) {
		cout << "Exception :" << e.what() << endl;
	}
	return 0;
}

在这里插入图片描述
接下来我们将exception转换为RangeException,再调用RangeException的成员函数进行异常信息查询。

int main()
{
	Vec3D v1 {1.2,1.3,1.4};
	try {
		cout << v1[4];
	}
	catch (exception & e) {
		cout << "Exception :" << e.what() << endl;
		if (typeid(e) == typeid(RangeException)) {
			auto r = dynamic_cast<RangeException &>(e);
			cout << "Vector Dimension :" << r.getDimension() << endl;
			cout << "Index: " << r.getIndex() << endl;
		}
	}
	return 0;
}

在这里插入图片描述

捕获多种无关异常

不同的异常的捕获

try块中的代码可能会抛出不同类型的异常:
注意,throw出去的是对象,这里我们创建的匿名对象。

class EA: public exception { };
class EB: public exception { };
class C {
public:
  void foo(int x) {
    if (x == 1)
      throw EA();
    else if (x == 2)
      throw EB();
  }
};

而一个catch块只能捕获一种异常:

int main() {
  C c { };
  try {
    c.foo(1);
    c.foo(2);
  } catch (EA& a) {
    cout << a.what() << endl;
  } catch (EB& b) {
    cout << b.what() << endl;
  }

捕获派生异常

派生异常类:

class MyException: public logic_error { };

catch参数类型为基类异常类型,则可以匹配:能捕获基类对象、也能捕获派生类对象

try {
  throw MyException(); // 抛出派生异常对象
} catch (logic_error& e) {  // catch参数为基类异常,但可以捕获所有派生类异常对象
  MyException* p = dynamic_cast<MyException*>(&e); // 转指针失败不会再抛异常
  if (p != nullptr)
    cout << p->what() << endl;
  else
    cout << e.what() << endl;
}

之前也提到过:dynamic_cast(obj)

  1. 若转型失败且NewType是指针类型,则返回nullptr。
  2. 若转型失败且NewType是引用类型,则抛出std::bad_cast类型的异常

异常处理的次序

捕获异常的正确次序:

派生类的catch块在前、基类的catch块在后

这种写法是错误的:

// (a)
try {
  ...
} catch (logic_error& e) {
  ...
} catch (MyException& e) {
  ...
}

例子:多重捕获异常类

task1:基于Vec3D类、RangeException异常类修改
1.1:将Vec3D的维数抽取出来
1.2:将RangeException改为继承 out_of_range
task2:添加ZeroException,当向量除以一个数为0时抛该异常
该异常应该继承runtime_error
task3:重载operator / () ,为Vec3D类添加标量除法(向量除以一个数)
当除数为0.0时抛异常。
根据IEEE 754 rules:
x > 0.0 : x/0.0 = INF
x < 0.0 : x/0.0 = -INF
0.0 / 0.0 = NaN

Vec3D.h:

#pragma once

#include <array>
#include <string>
#include <cmath>
#include <limits>
#include "RangeException.h"
#include "ZeroException.h"
class Vec3D {
public:
	constexpr static std::size_t DIMENSION = 3;
	
private:
	std::array <double, DIMENSION> v{1.0,1.0,1.0};
	bool AreSame(double a, double b) {
		return std::fabs(a - b) < std::numeric_limits<double>::epsilon();
	}
public:
	Vec3D() = default;
	Vec3D(double x, double y, double z){
		v[0] = x;
		v[1] = y;
		v[2] = z;
	}
	double &operator [] (const int index) {
		if(index >=0 && index < DIMENSION) {
			return v[index];
		}
		else {
			throw RangeException(DIMENSION,index);
		}
	}
	Vec3D operator /(const double divisor) {
		//构造当前对象的拷贝
		Vec3D t(*this);
		if (AreSame(divisor,0.0))
			throw ZeroException();
		for (auto &i : t.v) {
			i /= divisor;
		}
		return t;
	}
	
};

RangeException.h:

#pragma once

#include <iostream>
#include <exception>
class RangeException : public std::out_of_range {
private:
	std::size_t dimension{ 0 };
	int index{ 0 };
public:
	RangeException (std::size_t dimension,const int index) : out_of_range("index exceeds Vector dimension"){
		this->dimension = dimension;
		this->index = index;
	}
	std::size_t getDimension() {
		return dimension;
	}
	int getIndex() {
		return index;
	}
};

ZeroException.h:

#pragma once

#include <stdexcept>
#include <exception>

class ZeroException : public std::runtime_error {
public:
	ZeroException() : runtime_error("Divided by 0.0") {};
	ZeroException(const char* msg) : runtime_error("Divided by 0.0") {};
};

main.cpp:

#include <iostream>
#include <exception>
#include "RangeException.h"
#include "Vec3D.h"
using namespace std;

int main()
{
	Vec3D v1 {1.2,1.3,1.4};
	try {
		cout << (v1 / 0.0)[0] << endl;
	}
	catch (RangeException & e) {
		cout << "Exception :" << e.what() << endl;
		cout << "Vector Dimension :" << e.getDimension() << endl;
		cout << "Index: " << e.getIndex() << endl;
	}
	catch (ZeroException & e) {
		cout << "Exception :" << e.what() << endl;
	}
	return 0;
}

效果:
在这里插入图片描述

catch块的参数类型可以不用引用类型吗?

1、catch () 括号中的异常对象参数可否不用引用类型?
2、catch () 括号中的异常对象参数可否使用指针类型,比如: catch (Exception* e)
3、在多重异常捕获的代码中,若几个catch()括号中的参数是某个类继承链中不同层次的类的对象,此时括号中的参数可否不用引用类型?为什么?

1、catch()括号中的异常对象参数 可以使用:

1对象指针 catch(Exception * o)

2对象引用catch(Exception & o)

3一个对象catch(Exception  o) < >

2、 在多重异常捕获的代码中,若几个catch()括号中的参数是某个继承链中不同层次的类的对象,此时括号中的参数可以不用引用或指针类型,编译可以通过。

catch()参数如果是对象会发生什么?

1、会发生数据成员丢失

继承链上多态polymophic(虚函数的动态绑定)只有使用对象引用、指针类型才能发生。
当一个子类对象赋值给一个父类对象,这时放生的是对象之间数据成员间的拷贝,如果子类中比父类多出了一些数据成员,多出的数据成员会被截断丢弃,并且通过被赋值后的父类对象调用的虚方法只能是父类本身的虚方法,无法调用子类的虚方法,这是静态绑定。

2、会发生浅拷贝

形参与实参两个异常类对象的对象指针数据成员,指向堆中同一个new出来对象。
发生拷贝构造之后,原来作为catch()函数实参的异常对象要被销毁(先析构再收回空间),异常对象中对象指针指成员指向堆中new出来的对象地址空间被收回。但是作为catch()函数形参的异常对象还在,当形参离开catch块作用域范围前也需要被析构,并其离开catch块时将形参异常对象POP从栈中弹出释放空间。而在形参异常对象被析构时,需要先delete已经在堆中已经回收的对象地址,所以删除一个不存在的地址程序会出错。

3、·作为throw出的异常应该是对象,有可能被catch形参及块内数据覆盖:

在函数作用域范围内,被抛出的异常对象作为本地变量,本地变量存储在栈中,栈存储规律是先入后出,并且地址的大小排列顺序由高向低,而栈帧寻址方式重栈顶(低地址)到栈底(高地址)。

Throw抛异常是逐渐出栈过程,当遇到匹配的catch函数时停止出栈,转入catch作用域中。

也就是说throw出的异常对象高悬在栈顶,但是异常对象的空间已经被收回(对象已经POP出栈)。

但是异常对象存储的数据成员还存在与栈中,由与栈的写入数据序是有高向低地址,被throw抛出的异常对象地址在栈顶最小地址,所以很难被其他数据覆盖。

在catch函数作用域中只要抓取到被throw抛出的异常对象地址,就可以使用这个异常对象中的数据成员。

如果catch函数的形式参数如果是对象,形参对象也要入栈分配空间,这有可能会导致被throw抛出的异常对象数据被覆盖,所以使用对象指针或引用作为catch()函数的形参比较安全,指针或引用占用栈的空间比较小