资源管理
资源的一个特点是:使用完后必须还给系统。如果不归还糟糕的事情就会发生。
C++程序常见的使用资源有:
- 动态分配内存
- 文件描述符(file descriptors)
- 互斥锁(mutex locks)
- 图形界面的字型、笔刷
- 数据库连接
- 网络sockets
比如动态分配的内存,如果不归还,会导致内存泄漏。
Item 13 : 以对象管理资源
这里以一个投资类为例,工厂模式也是我们非常常用的一种设计模式,会很直接的遇到资源管理的问题。
class Investment { ... }; //投资类型 -->继承体系中的root class
Investment* CreateInvestment(); //工厂函数,返回指针
void f()
{
Investment *pInv = CreateInvestment();
...
delete pInv;
}
看起来妥当,然而存在一些潜在的问题:
- 将资源的释放交给客户,客户可能并不知情;
- 即使客户知道需要手动释放,也可能因为控制流的过早结束或发生异常而无法触碰到
delete语句。比如在f函数中的...中提前return或抛出异常,那这个指针将永远丢失,导致内存泄漏; - 即使你足够小心,其他人维护代码添加
return语句却忘了添加delete。
是否有一种方式,能自动回收资源,而不是煞费苦心考虑delete的位置,考虑每一个跳出位置和次数?
我们知道栈对象作用域结束后调用析构函数自动回收,资源想要这样应当如何呢?
答案正是将资源放入管理对象中,析构函数负责资源回收,以实现自动回收资源。
两个关键点:
- 获得资源后立刻放进管理对象
- 管理对象运用析构函数确保资源被释放
这样的技术称为RAII(Resource Acquisition Is Initialization) ——资源获取时初始化
STL提供了智能指针似乎正是为RAII而设计
void f()
{
std::shared_ptr<Investment> pInv(CreateInvestment());
std::unique_ptr<Investment> pInv2(CreateInvestment());
}
其中std::shared_ptr是引用计数型智能指针,持续追踪对象对象指向某资源,当无人指向它时自动删除该资源;
std::unique_ptr是独占型智能指针,仅有该指针唯一指向某资源,不可被复制。
要注意的是:智能指针默认析构函数做delete而不是delete[],因此对于数组不能进行这样的动作。对于其他类型的资源,比如文件描述符等,定制shared_ptr的删除器,使析构函数调用该删除器完成自动释放。
T* delete_func()
{
//(*T).close();
delete T;
}
std::shared_ptr<T> pInv(CreateT(), delete_func); //定制删除器
Item 14 : 在资源管理类中小心copy行为
如果智能指针不能满足你的需求,就需要建立自己的资源管理类,RAII机制是基础,关键在于,面对RAII对象被复制,会发生什么?
- 禁止复制。很多时候允许RAII对象被复制并不合理,参考
std::unique_ptr,禁止复制的方式参考Item 6 - 对底层资源使用引用计数法。有时候我们需要保有资源,直到它的最后一个使用对象被销毁,参见
std::shared_ptr - 复制底部资源。进行深拷贝,实际底部资源产生了新的附件。
- 转移底部资源的拥有权。
std::unique_ptr禁止复制构造和赋值,唯一的例外是从函数中return unique_ptr,同时它能转移所有权,通过std::move实现。这种情况确保只有一个TAII对象指向一个资源,被复制时仅仅是将资源的所有权转到新目标。
请记住:
- 复制RAII对象必须复制它所管理的资源,所有资源的copying行为决定RAII对象的copying行为;
- 常见的RAII class copying行为是:禁止copying,使用引用计数法。
Item 15 : 在资源管理类中提供对原始资源的访问
资源管理类很棒,但这个世界并不完美,不是所有的API接口都使用资源管理类,许多APIs直接指向资源,比如
std::shared_ptr<Investment>pInv(CreateInvestment()); //RAII
假如你希望某个函数处理Investment对象
int daysHeld(const Investment* pi); //返回投资天数
想要这样调用它
int days = daysHeld(pInv); //错误
无法编译通过,因为daysHeld需要的是Investment* 指针,传递的类型却是std::shared_ptr<Investment>对象
智能指针提供了转化为原始资源的方法
int days = daysHeld(pInv.get());
也有隐式转换使用operator->或 operator*访问
因此如果你想实现自己的资源管理类,需要提供取得原始资源访问的方法,可能是显式转化,也可能是隐式转化。显式转化比较安全,而隐式转化对客户比较方便。
TODO
显示转化和隐式转化的讨论
Item 16 : 成对使用new/delete时要使用相同形式
条款中的形式指的是是否带中括号,比如
int *pa = new int();
delete pa;
int *pa = new int()[100];
delete [] pa;
这样才能释放正确个数的对象。
Item 17 : 以独立语句将newed对象置入智能指针
假设有个函数用来揭示处理程序的优先权,另一个函数用来在某动态分配所得的Widget上进行某些带有优先权的处理:
int priority();
void processWidget(std::shared_ptr<Widget> pw, int priority);
谨记RAII,现在考虑调用processWidget()
processWidget(std::shared_ptr<Widget> pw(new Widget), priority());
然而,尽管我们谨记RAII而且遵循,这个函数的调用却依然有内存泄漏的风险。
在编译器角度考虑这个问题,编译器调用processWidget之前,先处理processWidget的实参:
- 调用
priority() - 执行
new Widget - 调用
std::shared_ptr的构造函数
C++按什么样的次序完成这些事情呢?和其他语言不同,函数参数的核算处理没有特定的顺序,我们知道的是new Widget在调用std::shared_ptr构造之前,但priority()的调用可能在任意顺序位置;
如果发生这样的情形:
- 执行
new Widget - 调用
priority() - 调用
std::shared_ptr构造函数
而巧合的是此时priority()抛出异常,会发生什么?它没有放入智能指针中,因此这种情形将引发内存泄漏。
解决方法也非常简单,将两个语句分离:
std::shared_ptr<Widget> pw(new Widget); //在单独语句中以智能指针托管newed对象
processWidget(pw, priority); //这样调用将绝不会导致内存泄漏
总结:以独立语句将newed对象存储于智能指针内,如果不这样做,一旦异常被抛出,有可能导致难以察觉的内存泄漏。

浙公网安备 33010602011771号