为什么要用对象去管理资源?
考虑如下情况。
我们声明一个对象A:
class A
{
private:
int value;
public:
A(int v):value(v) {};
A() {
cout << "creat A" << endl;
}
~A()
{
cout << "delete A" << endl;
};
};
然后在一个函数中使用:
void test()
{
A *a = new A(1);
......
delete a;
}
可以看到,
new
和delete
中间还有很多的执行语句,如果在这些执行语句中,触发了一个条件导致了return
的提前出现,然后这个函数就会被提前中止,delete
就不会被执行。这看起来妥当,但在若干情况下,或许因为”…”中的某个过早的return语句。如果这样一个return语句被执行;又或者是因为”…”中有异常抛出;这些情况发生吃,那么控制流一定就不会触及到delete语句;而之前申请的内存就无可避免的泄露了。
当然了,谨慎的编写程序可以防止这一类错误。但是有个问题是必须值得思考的,随着业务逻辑的更改,这段代码很可能会在时间渐渐过去之后被修改。一旦软件开始接受维护,很有可能会有某些人添加return语句或者continue语句而未能全然领悟它对函数资源管理策略造成的后果。
这时候,我们需要将资源来视作一个对象来看。当构造初始化的时候获取资源,当析构销毁的时候释放资源。而析构函数是依赖C++的自动调用机制,这样我们就可以确保资源被释放。
—《Effective C++》
这会导致怎样的后果?
a
指向的内存将没有任何方式可以进行访问,a
指向的对象将不会被正确的析构。而且,其依然占据着堆上的内存空间和其内部保存的指针指向的内存空间,这就导致了严重的内存泄漏和内存浪费
为了确保a指向的内存一定会被释放掉,我们需要将资源放进对象内部,然后当控制流离开test()
函数,该对象的析构函数就会被自动的调用然后去释放那些资源。
怎样去做?获得资源后立刻就将对象放进管理对象(这也被称作RAII
)。
我们试着去自己构造一个资源管理对象。
如下:
template<typename T>
class Data
{
private:
T* pointer;
public:
Data(const Data& d) :pointer(d.pointer) {};
Data() :pointer(NULL) {};
Data(T* p=NULL) :pointer(p) {};
~Data()
{
delete pointer;
}
T& operator*()
{
return *(this->pointer);
}
};
可以看到,这是一个简单的保存数据内存的管理类,当析构的时候就会自动删除其管理的内存空间。
我还重载了其“*”
操作符,用以方便的资源操作。
下面是一个使用的示例:
void test()
{
Data<int>p(new int(3));
cout << *p << endl;
}
在资源被拿出的一瞬间,我们就将这个资源放入了资源管理类中进行保存,由于p
存在于栈空间,所以当test()
结束的时候,p
就会被释放,其析构函数就会被调用。
所以无论是什么原因,p所管理的资源都会被成功的释放掉。
不同的设计也对应着不同的资源类型,当我们使用不同的资源管理类的时候要牢记这一点。
对于下面的三种类型,我们在写拷贝构造和赋值操作的时候就一定要注意不同的写法。
- 如果是普遍性的资源(如
string
、vector
),在赋值的时候应该是资源的拷贝。(生成资源副本进行拷贝)- 如果是文件、连接设备这样的资源,在赋值的时候应该是资源支配权的转移。(转移内部资源指针所指向的资源的所有权)
- 如果是
socket
这样的资源,在赋值的时候应该是增加资源的引用数,当引用数为0的时候释放资源。
对于这三种的类型如何实现拷贝的控制,我就单开一章描写