C++ 中异常机制

C++ 中异常机制

1. 异常对象保存

异常对象保存,指的是当异常发生时,我们把它捕获下来,但并不马上处理,而是先存储起来。这样一来,我们就可以在程序最后,或者其他并行任务的进程、线程里再去处理它。

接下来,我们要了解两个问题,一是如何保存异常对象,二是如何在其他的函数中去处理已经保存的异常对象。

exception_ptr save_ex;  // 保存异常对象

class MyException
{
public:
	MyException(string err)
	{
		m_err = err;
	}

	void show_err() const
	{
		cout << m_err << endl;
	}

public:
	string m_err;
};

void my_function1()
{
	throw MyException("异常发生");
}

void handle_exception()
{
	// 我们只是将异常对象进行了保存,此时如果需要重新处理,我们就需要重新将其抛出并处理
	try
	{
		// 重新抛出保存的异常
		rethrow_exception(save_ex);
	}
	catch (const MyException &ex)
	{
		ex.show_err();
	}
}

void test01()
{
	try
	{
		my_function1();
	}
	catch (const MyException& ex)
	{
		// 正常的情况下,捕获到异常之后需要马上进行处理
		// ex.show_err();
		// 将捕获到的异常对象保存起来,在今后某个时机再去处理
		// 如何保存异常对象到 save_ex 中呢?
		// save_ex = make_exception_ptr(ex);
		save_ex = current_exception();
	}

	// test01 的最后处理异常
	handle_exception();
}

2. 栈回退及清理

当异常发生的时候,throw 语句后面的代码就不再执行了,也就是说,正常的函数执行流程被打断了。那这里就有一个问题:如果在 throw 之前,我们创建了 N 个对象,这 N 个对象都调用了构造函数。由于后续代码不再执行,这些对象该怎么销毁呢?

当异常被抛出时,编译器会保证栈上创建的对象能够被正确地析构。
不过有些情况下,这种自动机制并不能保证所有对象都被及时释放,比如动态分配出来的对象。

class Student
{
public:
	Student()
	{
		cout << "Student 构造函数" << endl;
	}

	~Student()
	{
		cout << "Student 析构函数" << endl;
	}
};

void test02()
{
	// 使用智能指针来管理动态对象,从而避免异常机制的栈回退过程中,内存泄漏问题的发生。
	unique_ptr<Student> s(new Student);

	throw 1;
	cout << "hello world" << endl;
}

3. 异常处理流程

  1. 当调用 throw 抛出一个异常的时候,编译器会保存异常对象地址、析构函数地址、异常对象的类型信息。
  2. 执行栈回退、清理工作。
  3. 编译器判断发生异常的位置是不是在当前函数中,以及在当前函数中的哪个 try 块中,然后匹配 catch 块,并进入执行。
  4. 如果在当前函数中没有找到发生异常的 try,或者 catch 没有匹配上异常,就会将异常上抛给当前函数的调用者。
  5. 接下来,会再执行栈回退、清理,继续查找和匹配 try…catch 语句块。
  6. 最终都没有匹配到,编译器就会调用 std::terminate 函数,将当前程序终止。

在这个过程中,我们得到一条重要信息:在栈回退过程中,析构函数的调用不建议再抛出异常。原因有两点。

  1. 当在析构函数中再抛出异常,会导致 throw 后续的代码无法被执行。假设后续是一些非常重要的清理工作,就无法完成了。
  2. 栈回退的过程中,本来就在处理一个异常。此时在原来异常还没有处理完的情况下,再抛出一个新的异常,可能会导致程序直接终止。

可是析构函数中难免要写一些处理代码,我也不能保证析构函数一定不会发生异常。那这种时候该怎么办呢?

  1. 把析构函数中可能发生异常的代码放到 try 块中,自己消化吸收掉。
  2. 判断一下当前有没有异常产生,如果没有的话,再酌情抛出异常。
class Teacher
{
public:
	Teacher()
	{
		cout << "Teacher 构造函数" << endl;
	}

	~Teacher()
	{
		cout << "Teacher 析构函数" << endl;
#if 0
		try
		{
			throw - 2;
		}
		catch (...)
		{

		}
#endif
		// uncaught_exception 判断是否有异常产生
		// 如果为 true, 说明有异常产生
		// 如果为 false , 说明没有异常产生
		if (uncaught_exception())
		{
			cout << "有异常抛出" << endl;
		}
	}
};

void A()
{
	Teacher t;
	throw -1;
}

构造函数中允许抛出异常吗?
是允许的。构造函数负责对象的构建过程,由于它没有返回值,所以我们无法直接知晓对象构建过程中产生的问题。
这时候,就可以在构造函数中抛出一个异常,用来返回对象构建过程中的错误。

从上面学习到的异常机制中,我们能够明白:异常机制是一个较为复杂的容错机制。它能够大大提高代码的健壮性,但同时也会带来一些额外的开销。