13. 以对象管理资源
1 2 3 4 5 6
| void f() { Investment *pInv = createInvestment(); ··· delete pInv; }
|
- 可能无法执行delete
- 在···中过早return
- 若delete位于某循环中,由于continue或者goto语句过早退出
- ···内抛出异常
- 泄露的不只是内含投资对象的那块内存,还包括那些投资对象所保存的任何资源
- 为确保createInvestment返回的资源总是被释放,我们需要将这些资源放在对象内,当控制流离开f,该对象的析构函数会自动释放那些资源
- 实际上这正是隐身于本条款背后的半边想法,把资源放进对象内,我们便可以依赖c++的析构函数自动调用机制确保资源被释放
- 许多资源被动态分配于heap内而后被用于单一区块或函数内,他们应该在控制流离开那个区块或者函数时被释放,标准程序库提供的auto_ptr正是针对这种形式而设计的特制产品
- auto_ptr是个”类指针对象“,也就是智能指针,其析构函数自动对齐所指对象调用delete
1 2 3 4 5
| void f() { std::auto_ptr<Investment> pInv(createInvestment()); }
|
- 两个想法
- 获得资源后立刻放进管理对象内
- 管理对象运用析构函数确保资源被释放
- 由于auto_ptr被销毁时会自动删除它所指之物,所以一定要注意别让给多个auto_ptr同时指向一个对象。
- auto_ptrs有一个不寻常的性质,若通过copy构造函数或copy
assignment操作符复制他们,他们会变成null,而赋值所得的指针将取得资源的唯一拥有权
- 收auto_ptrs管理的资源必须绝对没有一个以上的auto_ptr同时指向它,意味着auto_ptrs并非管理动态分配资源的神兵利器。
- 例如:STL容器要求其元素发挥正常的复制行为,因此这些容器容不得auto_ptr
- auto_ptr的替代方案是”引用计数型智慧指针(reference-counting smart
pointer RCSP)
- 持续追踪共有多少对象指向某笔资源,并在无人指向它时自动删除该资源
- 类似于垃圾回收,但无法打破环状引用,例如两个其实已经没被使用的对象彼此互指,因而好像还处在“被使用"状态
- tr1::shared_ptr就是个RCSP
1 2 3 4
| void f() { std::tr1::shared_ptr<Investment> pInv(createInvestment()); }
|
- 复制行为正常
14.
在资源管理类中小心copying行为
- 资源取得时机便是初始化时机
- 并非所有资源都是heap_based,auto_ptr和tr1::shared_ptr这样的智能指针往往不适合作为资源掌管者,需要建立自己的资源管理类
- 例如,使用C
api函数处理Mutex的互斥锁现象,共有lock和unlock两函数
1 2
| void lock(Mutex* pm); void unlock(Mutex* pm);
|
1. 为确保绝不会忘记将一个被锁住的Mutex解锁,希望建立一个class用来管理机锁
2. 这样的class的基本结构由RAII守则支配,也就是”资源在构造期间获得,在析构期间释放“
1 2 3 4 5 6 7 8
| class Lock{ public: explicit Lock(Mutex* pm):mutexPtr(pm) {lock(mutexPtr);} ~Lock() {unlock(mutexPtr);} private: mutex *mutexPtr; };
|

3. 当一个RAII对象被复制,会发生什么事?
1 2
| Lock ml1(&m); Lock ml2(ml1);
|
1. 禁止复制。许多时候允许RAII对象被复制并不合理,因为很少能够合理拥有”同步化基础器物“的副本。如果复制动作对RAII class并不合理,便应该禁止。条款6:如果coipying操作声明为private
1 2 3
| class Lock: private Uncopyable{ public: };
|
2. 对底层资源基础”引用计数法“。有时需要保有资源,直到它的最后一个使用者(某对象)被销毁。这种情况下复制RAII对象时,应该将该资源的”被引用数”递增。tr1::shared_ptr是如此。
1. 通常只要内含一个tr1::shared_ptr成员变量,RAII classes便可出现reference-counting copying行为。
2. 如果Lock打算使用reference counting,可以改变mutexPtr类型,将Mutex* 改为tr1::shared_ptr<Mutex>。然而不幸的是tr1::shared_ptr的缺省行为是“当引用次数为0时删除其所指物“,这不是我们所要的行为。当我们用上一个Mutex,我们想要做的释放动作是解除锁定而非删除
3. 幸运的是tr1::shared_ptr允许指定所谓的”删除器”,那是一个函数或者函数对象,当引用次数为0时便被调用(此技能并不存在于auto_ptr——它总是将其指针删除)。删除器对tr1::shared_ptr构造函数而言时可有可无的第二参数,
4. 本例Lock class不声明析构函数,class析构函数会自动调用其non-static成员变量的析构函数。而mutexPtr的析构函数会在互斥器的引用次数为0时自动调用tr1::shared_ptr的删除器
3. 复制底部资源
1. 复制资源管理对象时,进行的时深度拷贝
2. 某些标准字符串是由指向heap内存之指针构成。那种字符串对象内含一个指针指向一块heap内存。当这样一个字符串对象被复制,不论指针或其所指内存都会被制作出一个复件。这样的字符串展现深度复制行为
4. 转移底部资源的拥有权
1. 某些罕见场合下你可能希望确保永远只有一个RAII对象指向一个未加工资源,即使RAII对象被复制依然如此。此时资源的拥有权会从被复制物转移到目标物。这是auto_ptr奉行的复制意义
2. copying函数有可能被编译器自动创建出来,因此除非编译器所生版本做了你想做的事,否则你要自己i安歇他们
- tips:
- 复制RAII对象把必须一并复制它所管理的资源,所以资源的copying行为决定RAII对象的copying行为
- 普遍而常见的RAII class
copying行为是,抑制copying、施行引用计数法。不过其他行为也都可能被实现
15.
在资源管理类中提供对原始资源的访问
- 资源管理类是对抗资源泄露的堡垒,在一个完美世界中依赖这样的classes处理和资源之间的所有互动,而不是直接处理原始资源
1 2
| std::tr1::shared_ptr<Investment>pInv(createInvestment()); int dayHeld(const Investment* pi);
|
- 需要一个函数可将RAII
class对象转换为其所内含之原始资源。有两个做法可以达成目标:显示转换和隐式转换
- 显示转换tr1::shared_ptr和auto_ptr都提供一个get成员函数,用来执行显示转换,也就是它返回智能指针内部的原始指针(的复件)
1
| int days = daysHeld(pInv.get());
|
- 就像几乎所有智能指针一样,tr1::shared_ptr和auto_ptr也重载了指针取值和操作符,它们允许隐式转换至底部原始指针
1 2 3 4 5 6 7 8 9 10 11 12
| class Investment{ public: bool isTaxFree() const; } Investment* createInvestment();
std::tr1::shared_ptr<Investment> pil(createInvestment());
bool taxable1 = !(pil->isTaxFree()); std::auto_ptr<Investment> pi2(createInvestment());
bool taxable2 = !((*pi2).isTaxFree());
|
- 取得RAII对象内的原始资源,提供一个隐式转换函数
1 2 3 4
| class Front{ public: FontHandle get() const {return f;} };
|
这使得客户每次使用API必须调用get
1 2 3 4
| void changeFontSize(FontHandle f, int newSize); Font f(getFont()); int newFontSize; changeFontSize(f.get(),newFontSize);
|
提供隐式函数
1 2 3 4 5 6 7 8 9
| class Font{ public: operator FontHandle() const {return f;} };
Font f(getFont()); int newFontSize; changeFontSize(f,newFontSize);
|
- 但是这个隐式转换会增加错误发生机会,例如客户可能会在需要Font时意外创建一个FontHandle
1 2
| Font f1(getFont()); FontHandle f2 = f1;
|
- 以上程序有个FontHandle由Font对象f1管理,但那个FontHandle也可通过直接使用f2取得。那几乎不会有好下场,例如当f1被销毁,字体被释放,而f2因此成为"虚吊的"dangle。
- 是否该提供一个显示转换函数(例如get函数)将RAII
class转换为其底部资源,或是应该提供隐式转换,答案主要取决于RAII
class被设计执行的特定工件,以及它被使用的情况。最佳设计可能是坚持条款18的忠告”让接口容易被正确使用,不易被误用“。
- 通常显示转换函数如get是比较受欢迎的路子,因为它将非故意之类型转换的可能性最小化了,然而有时,隐式类型转换所带来的自然用法也会引发天秤倾斜
- RAII
class内的那个返回原始资源的函数,与”封装“发生矛盾,但一般而言不是什么设计灾难。RAII
class并不是为了封装某物存在,它们的存在是为了确保一个特殊行为——资源释放——会发生。如果一定要,当然也可以在这基本功能之上再加一层资源封装,但并非必要。
- 也可以和身份松散的底层资源封装,获得真正的封装实现
- 例如tr1::shared_ptr将它所有计数机构封装了起来,但还是让外界很容易访问其所内含的原始指针。就像多数设计良好的classes一样,它隐藏了客户不需要看的部分,但备妥客户需要的所有东西
- APIs往往要求访问原始资源,所以每一个RAII
class应该提供一个取得其所管理之资源的办法
- 对原始资源的访问可能经由显示转换或隐式转换。一般而言显示转换比较安全,但隐式转换对客户比较方便
16.
成对使用new和delete时采用相同形式
- 当使用new时,有两件事发生
- 内存被分配出来,通过名为operator new的函数,
- 针对此内存会有一个或更多析构函数被调用
- 当使用delete时,也有两件事发生
- 针对此内存会有一个或更多析构函数被调用,然后内存才被释放
- 即将被删除的指针,所指的是单一对象还是对象数组
- 单一对象的内存布局一般而言不同于数组的内存布局
- 数组所用的内存通常还包括数组大小的记录,以便delete知道需要调用多少次析构函数,单一对象的内存则没有这笔记录。

1 2
| delete stringPtr1; delete [] stringPtr2;
|
- 调用new时使用[],必须在调用delete时也用[],调用new时没有使用[],也不应该在对应delete时使用[]
1 2 3
| typedef std::string AddressLines[4]; std::string* pal = new AddressLines; delete [] pal;
|
- 尽量不要对数组做typedefs动作,可将其改为vector
17.
以独立语句将newed对象置入智能指针
1 2 3
| int priority(); void processWidget(std::str1::shared_ptr<Widget> pw, int priority);
|
- 由于”以对象管理资源“,processWidget决定对其动态分配得来的Widget运用智能指针(tr1::shared_ptr)
1
| processWidget(new Widget, priority());
|
- 不能通过编译,tr1::shared_ptr构造函数需要一个原始指针,但该构造函数是个explicit构造函数,无法进行隐式转换,要转换为智能指针
1
| processWidget(std::tr1::shared_ptr<Widget>(new Widget, priority());
|
- 虽然使用了对象管理式资源,却可能泄露资源
- 编译器在产出一个processWidget调用码之前,必须核算即将被传递的各个实参,
- 第二个实参只是一个单纯的对priority函数的调用,
- 第一个实参std::tr1::shared_ptr(new Widget)由两部分组成
- 执行”new Widget"表达式
- 调用tr1::shared_ptr构造函数
- 在调用processWidget之前,编译器必须创建代码
- 调用priority
- 执行“new Widget"
- 调用tr1::shared_ptr构造函数
- new
Widget执行于tr1::shared_ptr构造函数被调用之前,但priority的调用可以排在第一第二或者第三执行
- 万一priority调用异常,new
Widget返回的指针将会遗失,因为它尚未被置入tr1::shared_ptr内,后者使我们用来防止资源泄露的
- 资源被创建和资源被转换为资源管理对象之间发生了异常干扰
- 避免方法:使用分离语句,分别写出
- 创建Widget
- 将它置入一个智能指针内,然后再把那个智能指针传给processWidget
1 2
| std::tr1::shared_ptr<Widget> pw(new Widget); processWidget(pw, priority());
|
- 请记住,以独立语句将newed对象存储于智能指针内,如果不这样做,一旦异常被抛出,有可能导致难以察觉的资源泄露