所谓资源就是你用了它就一定要还回去,不然会产生很多问题。
全篇围绕着RAII(Resource Acquisition Is Initialiaztion)模式结合智能指针进行讲述如何管理资源
我们创建了对象之后,很可能会忘记delete它,或者因为某些原因无法delete它,最后导致内存泄漏。为了确保资源总是会被释放的,我们需要将对象资源放进对象内,当控制流离开,对象的析构函数会自动释放掉这些资源。
故依赖C++的析构函数自动调用机制,确保资源被释放。
智能指针auto_ptr正是利用析构函数自动对其所指对象调用delete。
关于智能指针可以看我这篇:c++ 四种智能指针介绍以及均手撸代码实现
以对象管理资源的关键思想就是:
1.获得资源后立刻放进管理对象内:初始化的时候就取得资源。(Resource Acquisition Is Initialiaztion RAII)
2.管理对象运用析构函数确保资源被释放:不论控制流如何离开区块,一旦对象被销毁其析构函数自然会调用,资源就能释放。
请记住:
为防止资源泄露。请使用RAII对象,它们在构造函数中初始化,在析构函数中释放资源。
两个常被使用的RAII classes分别是auto_ptr和shared_ptr。后者是较佳选择,因为其copy行为比较直观。若选择auto_ptr,复制动作会使它指向null。
有些时候auto_ptr或者shared_ptr可能不适合作为资源管理了,我们可能需要建立自己的资源管理类。
假设我们有这样一个RAII的类A。
类内部存了一个mutex*资源,在初始化的给mutex上锁,析构的时候解锁。
但是如果在他析构之前发生了copying行为会怎么样?
#include#include using namespace std; class A { public: A(mutex* pm) :mutexPtr(pm) { mutexPtr->lock(); }//获得资源 ~A() { mutexPtr->unlock(); }//释放资源 private: mutex* mutexPtr; }; int main() { mutex m; if(1) { A lock(&m);//锁定m A lock1(lock);//将lock复制到lock1上会发送什么?==》 unlock of unowned mutex } return 0; }
寄了,直接报错。
那么怎么解决?
目前提供以下解法:
1.禁止复制。只要禁止复制了就不会产生这个问题了,在根源上断绝问题发生。
2.对底层资源进行引用计数,使用shared_ptr封装一下即可。但有个问题,shared_ptr的缺省行为是”当引用计数为0则删除其所指物”,但我们只是希望metex解锁而已。这个问题的解决方案即是指定”删除器“(deleter),这是一个函数或者函数对象,当引用次数为0的时候会调用它。
#include#include using namespace std; void unlock(mutex* x) { std::cout << "Deleter function called" << std::endl; x->unlock(); } class A { public: A(mutex* pm) :mutexPtr(pm, unlock) { mutexPtr.get()->lock();//获得资源 } private: shared_ptr mutexPtr; }; int main() { mutex m; if(1) { A lock(&m);//锁定m A lock1(lock);//将lock复制到lock1上会发送什么?==》 Deleter function called } return 0; }
3.复制底层资源。通过深拷贝来复制对象,这样就能对一份资源(这里是mutex*)进行任意次数的复制
4.转移底部资源拥有权。只需要把锁转移到另一个对象即可。
请记住:
复制RAII对象必须一并复制它所管理的资源,所以资源的copying行为决定RAII对象的copying行为
普遍而常见的RAII class copying行为是:抑制copying,施加引用计数法。不过其他行为也都可能被实现。
假如你希望使用资源管理类中管理的资源,那么资源管理类最好能提供对原始资源的访问,比如shared_ptr/auto_ptr中的get()函数返回的就是资源的显示转换的原始指针。当然(->和*操作符)也能允许它们隐式转换成底部原始指针。
关于显示转换和隐式转换的区别:
显示转换就是强转,隐式转换是自动转换
显示转换需要写一个.get(),隐式转换则是直接使用即可
#includeusing namespace std; class Resource { }; class ResourceManger { public: ResourceManger(Resource resource) :m_resource(resource) {} Resource get() const { return m_resource; }//显示转换 operator Resource() const { return m_resource; }//隐式转换 private: Resource m_resource; }; void UseResource(Resource resource) { cout << "use Resource" << endl; } int main() { Resource resource; ResourceManger rm(resource); UseResource(rm.get());//显示转换 UseResource(rm);//隐式转换 return 0; }
但这样隐式转换会有安全问题
因为你可以这样写:Resource rm1 = rm;
哇!本来应该是只能这样ResourceManger rm1 = rm;写的,但是你可以隐式转换,所以可以让rm隐式转换成Resource,然后才复制给rm1。这种的话rm1和rm共享了一份资源,当rm销毁时,rm1里面字体也会销毁!
所以我们应该让接口容易使用,不易被误用。所以还是最好不要用隐式转换了!
请记住:
APIs往往要求访问原始资源,所以每一个RAII class应该提供一个”取得其所管理的资源”的办法
对原始资源的访问可能经由显示转换或隐式转换。一般而已显示转换比较安全,隐式转换对客户方便。
这一条款很简单,我随便简单提一下即可。
new要搭配delete
new [n] 要搭配 delete []
如果不一一对应使用会发生未定义事件。
请记住:如果你在new表达式中使用[],必须在相应的delete表达式中也使用[]。如果你在new表达式中不使用[],一定不要在相应的delete表达式中使用[]
条款十七:以独立语句将newed对象置于智能指针此条款也简单,我也简单提一下。
我们看这一段代码
#includeusing namespace std; class A { public: A() { cout << "A" << endl; } }; void Process(shared_ptr a, int priority) { } int priority() { cout << "priority" << endl; return 1; } int main() { Process(shared_ptr(new A()), priority()); return 0; }
15行这一句Process(shared_ptr (new A()), priority());
执行了三个事情:
1.调用priority
2.执行new A
3.调用shared_ptr的构造函数
由于编译器的不同,我们是无法知道他们的调用次序的,假设是按照这样的顺序
new A -》 调用pirority -》调用shared_ptr的构造函数
那么如果在priority发生异常的话!!!那么A对象就不会被shared_ptr对象管理!
很有可能就会发生内存泄漏。
解决方案就是把new A和调用shared_ptr的构造函数放一起,让他们绑定在一起同时执行。
调用pirority则单独出来。
shared_ptr a = shared_ptr(new A()); Process(a, priority());
请记住:
以独立语句将newed对象存储于智能指针内。如果不这样做,一旦异常被抛出,有可能导致难以察觉的资源泄露。



