资源管理

资源的一个特点是:使用完后必须还给系统。如果不归还糟糕的事情就会发生。
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()的调用可能在任意顺序位置;
如果发生这样的情形:

  1. 执行new Widget
  2. 调用priority()
  3. 调用std::shared_ptr构造函数

而巧合的是此时priority()抛出异常,会发生什么?它没有放入智能指针中,因此这种情形将引发内存泄漏。

解决方法也非常简单,将两个语句分离:

std::shared_ptr<Widget> pw(new Widget);     //在单独语句中以智能指针托管newed对象
processWidget(pw, priority);        //这样调用将绝不会导致内存泄漏

总结:以独立语句将newed对象存储于智能指针内,如果不这样做,一旦异常被抛出,有可能导致难以察觉的内存泄漏。


posted @ 2021-01-11 16:26  煜恒  阅读(234)  评论(0)    收藏  举报