-
前言
-
Effective C++中有一章专门用来总结
资源管理
,可以看出资源管理操作在C++中的坑之多。有坑就有不被坑的方法,所以有了做一篇简略总结的想法。 -
这里对于资源的定义是从
系统获取
,使用完后需要还给系统
的东西。最常见的便是heap-based memory。还有句柄资源,mutex locks,数据库连接等。这些东西的共同点便是如果使用后不释放,会导致系统资源量的减少,而且伴随很难定位到的系统不明占用问题。 -
对于资源类,获取和释放方法一般都需要成对使用,但是真正资源管理的情况会比较复杂。异常,资源的多次释放,代码维护的改动,以及大量的获取释放调用都会在不经意间造成bug。智能指针以及
RAII(Resource Acquisition Is Initalization)
技术便是改进管理的方法。
-
-
智能指针
-
auto_ptr
- auto_ptr的特点是在析构函数调用的时候会释放其中raw_pointer所指的资源
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18
//一个获取资源的类 class Resource() { public: static Resource* getResource(); } static Resource* getResource() { return new Resource(); } //需要使用到资源的方法 void foo() { //获取资源并使用auto_ptr管理 std::auto_ptr<Resource> pSource(Resource::getResource()) //使用资源 ... }//函数结束时会调用pSource析构函数,同时释放资源
- 为了防止多个auto_ptr指向同一资源,导致被释放多次,auto_ptr的特性是当调用拷贝构造函数或者赋值运算符复制它时,被复制的auto_ptr会变成nullptr,以
保证只有一个
auto_ptr指向同一个资源。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18
//一个获取资源的类 class Resource() { public: static Resource* getResource(); }; static Resource* getResource() { return new Resource(); } //需要使用到资源的方法 void foo() { //获取资源并使用auto_ptr管理 std::auto_ptr<Resource> pSource_01(Resource::getResource()); //现在pSource_01指向nullptr,pSource_02指向原资源 std::auto_ptr<Resource> pSource_02(pSource_01); }//函数结束时会调用pSource_02析构函数,同时释放资源
-
shared_ptr
- shared_ptr增加了
引用计数
,每次有新的shared_ptr指向同一个资源时计数会增加,当计数为0时自动释放资源。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26
//一个获取资源的类 class Resource() { public: static Resource* getResource(); } static Resource* getResource() { return new Resource(); } //需要使用到资源的方法 void foo() { //获取资源并交给shared_ptr管理,引用计数为1 std::shared_ptr<Resource> pSource_01(Resource::getResource()); //使用资源 ... //引用计数增加为2,两个指针指向同一份资源 std::shared_ptr<Resource> pSource_02(pSource_01); //引用计数变为3 pSource_03 = pSource_02; //引用计数变为0,资源自动释放 pSource_01.reset(); pSource_02.reset(); pSource_03.reset(); }
- 注意auto_ptr和shared_ptr在其析构函数中所执行的是delete,而不是delete[]。所以对于数组类资源的管理完全可以通过string,vector等容器类来替代。容器提供了自动的容量扩展和内存管理。
- shared_ptr增加了
-
unique_ptr
- unique_ptr和auto_ptr的区别在于不允许拷贝构造和赋值,也就是
不允许复制
。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19
//一个获取资源的类 class Resource() { public: static Resource* getResource(); }; static Resource* getResource() { return new Resource(); } //需要使用到资源的方法 void foo() { //获取资源并使用auto_ptr管理 std::unique_ptr<Resource> pSource_01(Resource::getResource()); //错误 std::auto_ptr<Resource> pSource_02(pSource_01); pSource_02 = pSource_01 }//函数结束时会调用pSource_01析构函数,同时释放资源
- unique_ptr和auto_ptr的区别在于不允许拷贝构造和赋值,也就是
-
-
RAII
-
RAII技术的核心是
获取完资源就马上交给资源管理类
。前文所述三种类便是比较常用的RAII工具。如果需要自己设计资源管理类,则下面几个方面需要注意:-
对于拷贝的限制
- Q: 为什么要禁止拷贝?
- A: 对于一些资源的RAII类,复制行为本身是不合理的,像是系统mutex类如果进行了copy将增加管理的风险,和大量重复资源的占用。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22
//可以将复制构造函数和复制运算符声明为private,则无法调用复制 class NoCopyResource { private: NoCopyResource(const NoCopyResource& rhs); NoCopyResource& operator=(const NoCopyResource& rhs); }; //也可以创建Uncopyable基类 class Uncopyable { protected: Uncopyable() {}; ~Uncopyable() {}; private: Uncopyable(const Uncopyable& rhs); Uncopyable& operator=(const Uncopyable& rhs); }; //因为子类拷贝构造函数被调用的时候会先调用其父类拷贝构造函数,所以不能被拷贝 class NoCopyResource : public Uncopyable { ... };
-
深度拷贝, 复制底层资源
- Q: 拷贝RAII合理吗?
- A: 只要是你需要,且逻辑上合理都可以进行资源的复制。这个时候选择复制其RAII类,是为了确保不需要这个资源的副本时可以被RAII正确释放。
- Q: 什么情况需要深度拷贝
- A: 对象中存在需要RAII类管理的资源时,基本都需要深度拷贝。因为浅拷贝会造成两个对象中都有指向同一个资源的指针,如果其中一个析构时释放了资源,另一个变成为了野指针。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75
//这里用一个自定义string类做例子,资源类流程都类似 //资源类 class String { public: //RAII类通过这个接口获取原始资源 static String* MakeString(char* str) { m_str = new char(strlen(str) + 1); ... } ~String() { if(m_str) { delete [] m_str; m_str = nullptr; } } private: //无法通过构造函数来直接获取资源 String(char* str) (){}; static char* m_str; }; //管理类 class StringManager { public: String* GetString(char* str) { //RAII m_res = String::MakeString(str); return m_res; } ~StringManager() { //析构的时候因为m_res的存在会调用String类的析构函数 } StringManager(const StringManager& rhs) { m_res = new char(strlen(rhs.m_res)+1); strcpy(m_res, rhs.m_res); } StringManager& operator=(const StringManager& rhs) { //防止自我拷贝 if(m_res = rhs.m_res) return *this; else { delete[] m_res; m_res = new char(strlen(rhs.m_res)+1); strcpy(m_res, rhs.m_res); } return *this; } private: String* m_res; }; void foo() { StringManager copyManager; //StringManager作用域,离开便析构释放资源 { StringManager MyStrMgr; //进行RAII.p_mychar是其它地方或者系统提供的Raw资源 auto myStr = MyStrMgr.GetString(p_mychar); //进行深度拷贝 copyManager_01 = MyStrMgr; StringManager copyManager_02(MyStrMgr); } //离开作用域,MyStrMgr析构,资源myStr被释放,copyManager_02也析构释放资源副本 //copyManager_01仍然持有原资源副本 } //离开函数域copyManager_01析构释放资源副本
-
引用计数,deleter
- Q: 什么是deleter?
- A: 我们使用shared_ptr等引用计数器时,当引用次数为0时默认行为是删除所指向资源,但是有些时候我们想要的是
其他行为
,比如mutex类我们想要的行为是unlock当引用计数为0的时候。 这个时候就需要使用deleter。shared_ptr的第二个参数可以指定函数对象为deleter。
1 2 3 4 5 6 7 8 9 10 11 12
class StringManager { public: void GetString(char* str) { //这个用一个clear函数来做deleter,这种情况下都无需显示声明析构函数了 myStr = shared_ptr<String>(*(String::MakeString(str)), Clear) } void Clear(){...} private: std::shared_ptr<String> myStr; }
-
提供原始资源的Raw Pointer
- Q: 为什么需要提供Raw Pointer
- A: 虽然使用RAII的目的是隔绝用户和原始资源,但是现代API的设计避免不了通过RAII访问原始资源的需求。可以通过显式转换和隐式转换来提供Raw Pointer。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33
//auto_ptr和shared_ptr都提供了get()方法 class StringManager { public: void GetString(char* str) { //这个用一个clear函数来做deleter,这种情况下都无需显示声明析构函数了 myStr = shared_ptr<String>(*(String::MakeString(str)), Clear) } void Clear(){...} //显示转换 String& Get() { if(myStr->use_count > 0) return myStr->get() } //隐式转换 operator String() const { returen myStr->get(); } private: std::shared_ptr<String> myStr; } //假如有这么一个函数 UseStringDoSomething(String& str); //显式调用 StringManager myStrMgr; UseStringDoSomething(myStrMgr.Get()); //隐式调用 StringManager myStrMgr; UseStringDoSomething(myStrMgr); //隐式转换虽然方便用户使用,但是会有很多安全,应该根据实际情况来设计
-
smart pointer构造时使用独立的分离语句
1 2 3 4 5 6 7 8 9 10 11
//假设这里有个函数需要如下两个函数 void UseStringDoSomething(String& str, int nums) //为了使用RAII,一般进行如下调用 UseStringDoSomething(GetString(), GetNums()); //这里可能出现如下问题,调用GetString()时涉及到调用shared_ptr构造和MakeString()两步 //GetNums()和GetString()的调用顺序是不明的,如果GetNums在shared_ptr构造和MakeString()之间调用了,且出现异常中止,那就不可知结果. //所以正确做法 auto str = GetString(); UseStringDoSomething(str, GetNums());
-
-