Effective C++ 学习笔记(三)资源管理
参考书籍《Effective C++:改善程序与设计的 55 个具体做法(第三版)》
13. 以对象管理资源
-
为什么要?
- 对象很可能会被忘记 delete
-
示例
-
使用智能指针 auto_ptr,智能指针在其析构函数里对 delete 所指对象(是否要考虑多个智能指针指向同一个对象,导致一个对象被 delete 多次?智能指针若通过 copy 构造函数或 copy assignment 操作符复制它们,它们会变成 null,而复制所得的指针将取得资源的唯一拥有权!即受 auto_ptrs 管理的资源必须绝对没有一个以上的 auto_ptr 同时指向它)
Investment* createInvestment(); void f() { std::auto_ptr<Investment> pInv(createInvestment()); ... } // pInv的析构函数里,会delete所指对象
-
上面知道 auto_ptr 没办法做复制操作,如果真的需要做复制操作,auto_ptr 的替代方案是“引用计数型智能指针”(reference-counting smart pointer;RCSP)。所谓 RCSP 也是个智能指针,持续追踪共有多少对象指向某笔资源,并在无人指向它时自动删除该资源。RCSPs 提供的行为类似垃圾回收(garbage collection),不同的是 RCSPs 无法打破环状引用(cycles of references,例如两个其实已经没被使用的对象彼此互指,因而好像还处在“被使用”状态)。
RAII(Resource Acquisition IsInitialization),即资源在构造期间获得,在析构期间释放。
■ 为防止资源泄漏,请使用 RAII 对象,它们在构造函数中获得资源并在析构函数中释放资源。
■ 两个常被使用的 RAII classes 分别是 tr1::shared_ptr 和 auto_ptr。前者通常是较佳选择,因为其 copy 行为比较直观。若选择 auto_ptr,复制动作会使它(被复制物)指向 null。
-
14. 在资源管理类中小心 copying 行为
-
为什么要?
- 有些情况下确实需要自己建立一个资源管理类,而不是使用 auto_ptr 和 shared_ptr。
// RAII class class Lock{ public: explicit Lock(Mutex* pm) : mutexPtr(pm) { lock(mutexPtr); } ~Lock() { unlock(mutexPtr); } private: Mutext* mutexPtr; };
- 有些情况下确实需要自己建立一个资源管理类,而不是使用 auto_ptr 和 shared_ptr。
-
示例
- 如果复制动作对 RAII class 并不合理,你就应该禁止其的 copying 操作(copy 构造函数和 copy assignment 操作符)。根据条款 6,可以这样实现:
class Lock : private Uncopyable{ ... };
- 有时候我们希望保有资源,直到它的最后一个使用者(某对象)被销毁,就可以使用所谓的“引用计数型智能指针” tr1::shared_ptr。
class Lock{ public: explicit Lock(Mutex* pm) :mutexPtr(pm, unlock) //shared_ptr允许自定义删除器,这里我们使用unlock作为删除器,即当引用次数为0时,会调用pm->unlock() { lock(mutexPtr.get()); } private: std::tr1::shared_ptr<Mutex> mutexPtr; };
- 复制 RAII 对象,同时也复制其所包覆的资源。也就是说,复制 RAII 对象时,进行“深度拷贝”。
- 某些罕见场合下你可能希望确保永远只有一个 RAII 对象指向一个未加工资源(raw resource),复制时资源的拥有权应该从被复制物转移到目标物。这种行为可以由之前提到的 auto_ptr 所实现。
■ 复制 RAII 对象必须一并复制它所管理的资源,所以资源的 copying 行为决定 RAII 对象的 copying 行为。
■ 普遍而常见的 RAII class copying 行为是:抑制 copying、施行引用计数法(referencecounting)。不过其他行为也都可能被实现。
- 如果复制动作对 RAII class 并不合理,你就应该禁止其的 copying 操作(copy 构造函数和 copy assignment 操作符)。根据条款 6,可以这样实现:
15. 在资源管理类中提供对原始资源的访问
-
为什么要?
-
有许多 APIs 需要访问原始资源
std::tr1::shared_ptr<Investment> pInv(createInvestment()); int daysHeld(const Investment* pi); daysHeld(pInv); // 错误!需要Investment*,提供shared_ptr<Investment>
-
-
示例
- 智能指针显式转换,tr1::shared_ptr 和 auto_ptr 提供 get 成员函数,返回智能指针内部的原始指针
daysHeld(pInv.get())
- 智能指针隐式转换,就像(几乎)所有智能指针一样,tr1::shared_ptr 和 auto_ptr 也重载了指针取值(pointerdereferencing)操作符(operator->和 operator*),它们允许隐式转换至底部原始指针:
std::tr1::shared_ptr<Investment> pInv(createInvestment()); bool taxable = !(pInv->isTaxFree()); bool taxable = !(*(pInv).isTaxFree());
- RAII class 提供对原始资源的访问
class Font { public: explicit Font(FontHandle fh):f(fh){ } ~Font() { releaseFont(f)} FontHandle get() const {return f;} // 提供显式转换函数 operator FontHandle () const {return f;} // 提供隐式转换函数 private: FontHandle f; }; Font f(getFont()); changeFontSize(f.get(), newFontSize); FontHandle fh = f; // 隐式转换隐患!
■ APIs 往往要求访问原始资源(raw resources),所以每一个 RAII class 应该提供一个“取得其所管理之资源”的办法。
■ 对原始资源的访问可能经由显式转换或隐式转换。一般而言显式转换比较安全,但隐式转换对客户比较方便。
- 智能指针显式转换,tr1::shared_ptr 和 auto_ptr 提供 get 成员函数,返回智能指针内部的原始指针
16.成对使用new和delete时要采取相同形式
-
为什么要?
std::string* stringArray = new std::string[100]; delete stringArray; // 只delete了一个对象!
-
示例
- 告诉delete指针指向的是对象数组
std::string* stringArray = new std::string[100]; delete [] stringArray;
■ 如果你在new表达式中使用[],必须在相应的delete表达式中也使用[]。如果你在new表达式中不使用[],一定不要在相应的delete表达式中使用[]。
- 告诉delete指针指向的是对象数组
17.以独立语句将newed对象置入智能指针
-
为什么要?
- 将new和智能指针语句合并作为函数实参时,可能发生资源泄露
当执行顺序为void processWidget(std::tr1::shared_ptr<Widget> pw, int priority); processWidget(std::tr1::shared_ptr<Widget>(new Widget), priority());
1. 执行new Widget
2. 调用priority
3. 调用shared_ptr的构造函数
(C++函数参数核算的顺序是不固定的!)
当调用priority()发生异常时,new Widget返回的指针将会遗失,造成资源泄露!
- 将new和智能指针语句合并作为函数实参时,可能发生资源泄露
-
示例
- 将newed对象放入智能指针的过程独立出来,在这里就是与函数调用分开来
std::tr1::shared_ptr<Widget> pw(new Widget); processWidget(pw, priority());
■ 以独立语句将 newed对象存储于(置入)智能指针内。如果不这样做,一旦异常被抛出,有可能导致难以察觉的资源泄漏。
- 将newed对象放入智能指针的过程独立出来,在这里就是与函数调用分开来