C++ 异常那些小事儿~
1. 不要在释放资源或关闭句柄前,抛出异常
有的时候我们的程序会在各种各样的地方抛出异常,我们需要做的就是接收异常信息,然后去改进程序就行。但是有一种情况是无论如何都不能抛出异常的。那就是在释放资源之前不能抛出异常。
先看一个简单的代码:
#include <iostream>
#include <string>
double Division(int lhs, int rhs)
{
if (rhs == 0)
throw "the rhs is 0";
else
return (double)lhs / (double)rhs;
}
void Func()
{
int *array = new int[10];
int lhs, rhs;
std::cin >> lhs >> rhs;
Division(lhs, rhs);
std::cout << "delete[] array" << std::endl;
delete[] array;
}
void Test()
{
try
{
Func();
}
catch(const char* errmsg)
{
std::cout << errmsg << std::endl;
}
}
这个程序如果正常执行,那么不会有问题,并且动态申请的资源也会被释放
那如果除数是0呢,这个程序就会终止,并且刚才开辟的资源没有被释放。这就造成了严重的内存泄漏问题。
那么该怎么解决这个问题呢?
这里就要引入 异常的重新抛出 这个概念了。就是在原地接收任何异常,在这个里面将刚刚释放的资源重新释放。
我们只需要改一下Func
这个函数
void Func()
{
int *array = new int[10];
try
{
int lhs, rhs;
std::cin >> lhs >> rhs;
Division(lhs, rhs);
}
catch(...)//...表示接收任何异常
{
std::cout << "delete[] array" << std::endl;
delete[] array;
throw;//重新抛出异常信息
}
std::cout << "delete[] array" << std::endl;
delete[] array;
}
see?这里就可以解决内存泄漏的问题啦。
2. 在析构函数内要吞下异常
析构函数的道理跟上一条类似,就是如果过早的抛出异常,可能会导致内存泄漏。
那么我们有什么办法呢?
此条参考《effective C++》
一般为了避免客户在某种情况下忘记释放已经开辟的内存我们通常会定义一个类去管理它,以便在出了这个类的作用域之后,它会自动调用析构函数去释放资源,以免造成内存泄漏。
但是一旦在析构函数中发生异常,一般会有两种处理方法。
- 终止程序
template <class T>
class AutoPtr
{
public:
~AutoPtr()
{
try{
...
delete ptr;
}
catch(...)
{
...
abort();//调用abort()函数来终止程序
}
}
private:
T* ptr;
};
void DoSomething()
{
AutoPtr<int> p = new int[100];
}
因为往往异常会使程序发生不明确行为,那么终止程序就直接将这种不明确行为扼杀在摇篮里。
可是如果终止程序,有可能会使开辟的资源还没有被释放,那这不就造成了内存泄漏嘛。于是就有了第二种做法。
- 吞下异常
~AutoPtr()
{
try{
...
delete ptr;
}
catch(...)
{
...
//制作运转记录,记录下对资源释放时发生的异常。
}
}
但是,将异常吞下也是一个不好的做法,因为你压制了某些动作失败的重要信息。
所以,这两种做法其实都不怎么好,那么该怎么办呢?不妨我们再次将这个锅甩回给客户。我们定义一个接口来释放资源,让用户去调用。以便在这个接口内发生异常后,用户可以去处理。而且再加一个判断,如果客户没有调用接口去释放资源,我们在结束时再让析构函数去执行资源释放的动作。如果此时再出异常,就可以把锅甩给客户,谁让他没有调释放接口呢?
class AutoPtr
{
public:
...
void DeleteMem()
{
delete ptr;
isDelete = true;
}
~AutoPtr
{
if (!isDelete)
{
try{...}
catch(...){...}
}
}
private:
T* ptr;
bool isDelete = false;
}
3. 异常到底该怎么玩?
其实开发中,异常不是这样写的。在C++标准库中有一套自己的异常体系。他们是根据多态弄的。有一个基类exception
然后它下面继承了很多派生类,根据重写函数达到多态的操作。这样以后就可以直接抛出一个对象,然后根据对象自己重写的函数打印出自己的信息。
void Test()
{
try
{
std::vector<int> v(6, 1);//表示给v初始化6个1
v.reserve(10000000000000000);//内存肯定不够啦,这里就会抛出异常
//或者
v[6] = 2; //这很明显越界访问啦,也会抛出异常
}
catch(const exception& e)
{
std::cout << e.what() << std::endl;//打印出异常信息
}
catch(...)
{
//库里没有这个对象的类,所以就是未定义异常
std::cout << "unknow exception!" << std::endl;
}
}
那么库里到底有什么类呢?
看看这些全都是,还没完呢,也就剩几个了。但是有一个点就是,总会有一些异常类型他没有定义呀。所以一般公司里都是自己有一套异常体系去用的。