条款13~17 资源管理
条款13~17 资源管理
知识点 RAII对象
- 本大章节-资源管理,需要弄懂什么是RAII,在本博客《什么是RAII》中有详解
条款13 以对象管理资源
-
前言
-
其实在本文中的
auto_ptr已经被弃用了,建议去看《Effective Modern c++ Item18 ~ Item22》 -
但是在这里还是说一下该条款的一些思路点
-
-
思路点
-
在构造中获得资源并在析构函数中释放资源
-
两个常用的自动管理资源的类是shared_ptr和auto_ptr,其中auto_ptr的复制动作,会导致复制对象变为null,容易造成意外的错误,一般推荐使用shared_ptr,其使用引用计数的原理实现对象共享的目的,并且在计数为0时自动释放对象
-
Summary
- 已过时,建议去看《Effective Modern c++ Item18 ~ Item22》
- 思路点其实就是RAII原则
条款14 在资源管理类中小心copying行为
-
引言
- 上节是对资源的管理说明。有时候我们不能依赖于shared_ptr或者auto_ptr,所以我们须要自己建立一个资源管理类来管理自己的资源
-
假设我们使用
C API函数处理Mutex互斥器对象:1 2void lock(Mutex* pm); //锁定pm所指的互斥器 void unlock(Mutex* pm); //解除锁定为了忘记释放锁,我们想要建立一个class来进行资源管理,这里class应该满足
RAII守则,即"资源在构造期间获得,在析构期间释放"1 2 3 4 5 6 7 8 9 10 11class Lock{ public: explicit Lock(Mutex* pm):mutexPtr(pm){ lock(mutexPtr);//获得资源 } ~Lock(){ unlock(mutexPtr); // 释放资源 } private: Mutex* mutexPtr; };
-
copying行为
-
上述例子写的这很不错,但是如果此时
Lock对象被复制,会发生什么?1 2Lock ml1(&m); // 锁定m Lock ml2(ml1); // 将ml1复制到ml2身上这样做绝对不行,我们不能确定什么时候m2和m1会被析构,一旦被析构就会导致mutex解锁,mutex一旦解锁就会被别的进程所调用,程序将出现巨大的混乱
其实,这不是说一个特定例子,一般的情况是:当一个
RAII对象被复制时,应该如何选择,有以下两种做法
-
-
禁止复制
- 如条款06所言:将
copying操作声明为private,或者利用c++11新特性= delete
- 如条款06所言:将
-
引用计数
- 对底层资源使用”reference-count“
-
有时候希望保有资源,直到它的最后一个使用者(某对象)被销毁。对
Lock打算使用reference counting它可以改变mutexPtr类型,将Mutex*改为tr1::shared_ptr<Mutex>,当然这不是我们想要的,我们只是想释放锁,而不是删除锁 -
幸运的是
tr1::shared_ptr运行指定所谓的删除器,那是一个函数或对象,引用次数为0才被调用。删除器对tr1::shared_ptr构造函数而言是可有可无的第二参数,所以代码看起来像这样1 2 3 4 5 6 7 8 9class Lock { public: explicit Lock(mutex* pm) : mutexPtr(pm, unlock) { lock(mutexPtr.get()); } private: std::trl::shared_ptr<Mutex> mutexPtr; }
Summary
- 复制
RAII对象必须一并复制它所管理的资源,所以资源的copying行为决定RAII对象的copying行为 - 普遍常见的RAII class copying 行为是:禁止复制、施行引用计数法
条款15 资源管理类中提供对原始资源的访问
这里想要将RAIIclass对象进行转换为其内含之原始资源,有两种转换
-
示例代码
-
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20FontHandle getFont(); void releaseFont(FontHandle fh); class Font { public: explicit Font(FontHandle fh) : f(fh) { } ~Font() { releaseFont(f); } FontHanle get() const { return f; } // 显示转换 operator FontHandle() const { return f; } // 隐式转换 private: FontHandle f; }; Font f1(getFont()); FontHanle f2 = f1;
-
-
显示转换
- 提供
get()函数得到原始指针 - 显示调用 很不错 没有啥问题
- 提供
-
隐式转换
- 允许隐式转换,但是会有问题,在客户
FontHanle f2 = f1;的时候,并不知道这还有一层隐式转换的逻辑,所以会增加错误率
- 允许隐式转换,但是会有问题,在客户
Summary
- APIs往往要求访问原始资源,所以每一个RAII class应该提供一个”取得其所管理之资源“的方法
- 对原始资源的方法可能会经由显式转换或隐式转换
- 相对而言显式转换比较安全,但是频繁
get()很烦人 - 但隐式转换对用户比较方便
- 相对而言显式转换比较安全,但是频繁
- 个人觉得是显示调用要好,频繁调用
get()又不会死,隐式调用用户如果不告诉他,或者他自己发现,也不会察觉到有一次隐式调用,会增加报错的几率
条款16 new和delete 要使用相同形式
-
在使用
newdelete需要使用一致的形势使用,否则会导致内存泄漏1 2 3 4 5std::string* stringPtr1 = new std::string; std::string* stringPtr2 = new std::string[100]; ... delete stringPtr1; delete[] stringPtr2; -
要注意
typedef自定义类型1 2 3 4 5typedef std::string AddressLines[4]; std::string* pal = new AddressLines; // new string[4] delete pal; // undefined!!! delete[] pal; // fine这个规则对于使用
typedef的程序员来说十分重要,因为它意味着typedef的作者必须说清楚:当程序员以new创建该种typedef类型对象是,该以哪一种delete形式删除之,考虑一下这个typedef:为避免诸如此类错误,尽量不是对数组形式使用
typedef操作。这容易达成,因为C++标准程序库含有stringvector等templates,可以将数组的需求下降到0
Summary
- 在使用
newdelete需要使用一致的形势使用,否则会导致内存泄漏 - 在
typedef自定义类型的时候,一定要注释写着哪一种delete形式删除
条款17 以独立语句将newed对象置入智能指针
-
有些智能指针类(比如
shared_ptr<T>)不支持隐式类型转换,假设存在这样两个函数1 2void fun(shared_ptr<int> lhs,int rhs); int foo();那么对于以下函数调用
1fun(new int,foo());将不能通过编译,解决方法之一:
1fun(shared_ptr<int>(new int),foo());它共有三个步骤
- 执行
new int - 构造
shared_ptr<int> - 调用
foo()
但是由于编译器对于同一语句的各项操作具有重新排列的自由,因此除了执行new int肯定在构造
shared_ptr<int>之前外,调用foo(),可以发生在任何阶段,可能在最前,中间,最后,如果是按:执行new int→调用foo()→构造shared_ptr<int>的顺序执行,那么如果foo()发生异常,就会在shared_ptr<int>构造之前造成内存泄露. - 执行
-
对于1出现的问题,可以利用编译器对于"跨越语句的各项操作"没有重新排列的权力,以独立语句将newed对象置入智能指针,如下
1 2shared_ptr<int> ptr(new int); fun(ptr,foo);这样可以防止由于foo在new出的int被放入
shared_ptr<int>之前抛出异常而导致内存泄露,但要注意:- ptr不是临时对象,也就是说调用fun后ptr管理的内存没有被释放,而ptr的存在并不是为了要访问底层资源,而是为了防止出现异常而造成内存泄露存在的,所以如果不需要ptr所指向的内存,最好调用
reset()将它释放.(个人认为这个条款有些鸡肋)
- ptr不是临时对象,也就是说调用fun后ptr管理的内存没有被释放,而ptr的存在并不是为了要访问底层资源,而是为了防止出现异常而造成内存泄露存在的,所以如果不需要ptr所指向的内存,最好调用