Effective C++读书笔记三(资源管理)

18 篇文章 0 订阅
4 篇文章 0 订阅

所谓资源就是,一旦你用了它将来必须还给系统。
C++程序中最常使用的资源就是动态分配内存,但内存只是你必须管理的众多资源之一。例如:文件描述符、互斥锁、图形界面中的字型和笔刷、数据库连接及网络socket。不论是哪一种资源,当你不再使用它的时候,必须将它还给系统。

条款13:以对象管理资源

先来看个例子:

class Investment {...};   //继承体系中的root class

Investment* createInvestment(); 
//返回指针,指向Investment继承体系内动态分配的的对象,调用者有责任删除它。

//函数fun履行了这个责任
void fun()
{
    Investment* pInv=createInverstment();
    ...
    delete pInv;
}

这个例子乍一看没有什么问题,动态申请的对象在函数结尾也delete了,但有些情况fun可能无法删除pInv

  • 假设在“…”区域内有一个过早的return语句,如果这个return语句被执行了,那么程序就不可能会触及delete语句。
  • 还可能是在pInv的使用和delete动作在某个循环内,而该某个循环由于某个continue或者goto语句过早退出,也可能不会delete
  • 最后一种可能是“…”区域内的语句抛出异常,那么同样不会执行到delete

为了确保createInvestment返回的资源总是被释放,我们需要将资源放进对象内,当控制流立刻fun,该对象的析构函数会自动释放那些资源。
标准程序库提供的auto_ptr正是针对这种形式设计的特质产品,也就是所谓的“智能指针”。来看看这个对象的使用:

void fun()
{
    std::auto_ptr<Investment> pInve(createInverstment());
    ...
}

调用createInvestment函数,一如既往的使用pInv,经由auto_ptr的析构函数自动删除pInv。
以对象管理资源主要有两个关键想法:

  • 获得资源后立刻放进管理对象内
  • 管理对象运用析构函数确保资源被释放

由于auto_ptr被销毁的时会自动删除它所指之物,所以不要让多个auto_ptr指向同一个对象,如果这样做了,那么就会出现同样的对象被删除多次的情况。为了防止这个问题,auto_ptr有一个性质:若通过拷贝构造函数或赋值运算符重载复制它们,它们就会变成NULL,而复制所得的指针将取得资源唯一的拥有权。但这意味着auto_ptr并非管理动态内存的神兵利器,因为它无法有正常的复制行为,而我们有的时候需要这种行为,例如STL。
取代auto_ptr就是shared_ptr——–引用计数型智能指针,在下一个条款会详细介绍。
请记住:

  • 为了防止内存泄漏,请使用RAII对象,它们在构造函数中获得资源并在析构函数中释放资源

条款14:在资源管理类中小心copying行为

刚刚的条款13引入的RAII机制和智能指针,并非所有的资源都适用,对于一些不是heap_based的资源,智能指针往往不适合作为资源掌管者。那么这个时候,你就需要建立自己的资源管理类
例如下面这个类:

class Lock
{
public:
    explict Lock(Mutex* pm)
        :mutexPty(pm)
    {
        lock(mutexPtr);
    }
    ~Lock()
    {
        unlock(mutexPtr);
    }
private:
    Mutex* mutexPtr;
};

客户对Lock的用法符合RAII方式,但是如果Lock对象被复制会发生什么现象?

Mutex m;
Lock mu1(&m);  //锁定m
Lock mu2(mu1); //将mu1复制到mu2身上???

当一个RAII对象被复制,会发生什么事?大多数时候你会选择以下两种情况:

  • 禁止复制—-许多时候允许RAII对象被复制并不合理。那就可以将拷贝构造函数和赋值运算符重载声明为私有,而且不实现。—-unique_ptr
  • 底层资源用“引用计数法”—-也就是shared_ptr的实现方法。但是,有的时候我们并不希望析构的时候删除所指物,例如刚刚的mutex,我们想要做的释放动作是解除锁定。这就要用到“定制删除器”了。所谓的删除器也就是一个函数或函数对象。当引用次数为0时便被调用。

复制RAII对象必须一起复制它所管理的资源,所以资源的拷贝行为决定RAII的拷贝行为。

条款15:在资源管理类中提供对原始资源的访问

条款13说过,使用智能指针保存createInvestment函数的调用结果,假设你希望以某个函数处理Investment对象,像这样

int daysHeld(const Investment* pi);    //返回投资天数

你想这样调用它:

int days = daysHeld(pInv);   //错误

这样是不能通过编译的,因为daysHeld需要的是Investment*类型的指针,而pInv却是一个shared_ptr的对象,这时候你需要一个函数可以将RAII类对象转换为其内部的原始资源。有两个做法可以达成目标:隐式转换、显式转换
shared_ptr和auto_ptr内部都提供一个get成员函数,用来执行显式转换,也就是get函数会返回智能指针内部的原始指针。

int days = daysHeld(pInv.get());

还有一种方式是提供一个隐式转换函数,看下面的例子:

FontHandle getFont();            //这是一个C API,省略参数
void releaseFont(FontHandle fh); //来自同一组C API

class Font
{
public:
    explicit Font(FontHandle fh)
        :f(fh)
    {}
    ~Font()
    {
        releaseFont(f);
    }
private:
    FontHandle f;
};

假设现在有大量C API都要处理FontHandle,那么将Font对象转换为Fonthandle对象会是一种和频繁的操作,那么Font类就可以提供一个显示转换函数,像get那样:

class Font
{
public:
    FontHandle get()const
    { return f; }
    ...
};

但是可以想到,只有我们想使用API的时候就需要调用get函数,那么就可以令Font类提供隐式转换函数,将类型转为FontHandle:

class Font
{
public:
    operator FontHandle()const
    { return f; }
    ...
};

但是这种隐式类型转换有时候会出现错误,假设我们可能会在需要Font类型时,却意外创建了一个FontHandle类型。
是否应该提供一个显示转换函数还是隐式转换函数,取决于RAII类被设计执行的特定工作,以及它被使用的情况。一般而言显式转换比较安全,但隐式转换对客户比较方便。

条款16:成对使用new和delete时要采取形同形式

当你使用new的时候有两件事发生:

  • 内存被分配出来
  • 针对此内存会有一个或多个构造函数被调用

当你使用delete的时候也有两件事发生:

  • 针对此内存有一个或多个析构函数被调用
  • 内存被释放

delete的最大问题在于:即将被删除的内存里究竟存有多少对象?这个问题的答案决定了有多少个析构函数必须被调用。实际上这个问题可以更简单些:即被删除的那个指针,所指的是单一对象或对象数组?
当你对着一个指针使用delete,唯一能够让delete知道内存中是否存在一个数组的方法就是由你来告诉它。如果你使用delete时加上方括号[ ],delete便认定指针指向一个数组,否则它便认为指针指向单一对象。
也就是说,如果你调用new时使用[ ],那么对应的在调用delete时也使用[ ]。如果你调用new时没有使用[ ],那么也不应该在对应调用delete时使用[ ]。

条款17:以独立语句将new的对象置入智能指针

先来看个例子:

int priority();
void processWidget(shared_ptr<Widget> pw, int priority);

现在调用processWidget:

processWidget(new Widget, priority());

这个调用形式是不能通过编译的,因为shared_ptr构造函数需要一个原始指针,但该构造函数是个explicit构造函数,无法进行隐式转换,将得到“newWidget”的原始指针转换为processWidget所要求的shared_ptr。下面就可以通过编译:

processWidget(shared_ptr<Widget>(new Widget), priority());

来剖析一个这个式子:
编译器产生出一个processWidget调用码之前,必须先核对参数。上述第二个实参只是单纯对priority函数的调用,但第一个实参shared_ptr(new Widget)由两部分组成

  • 执行“new Widget”表达式
  • 调用shared_ptr的构造函数

编译器完成的这些事情是以什么顺序呢?不是很确定,但是可以肯定的是“new Widget”一定执行在shared_ptr构造函数之前,如果是下面的顺序:

  • 执行“new Widget”表达式
  • 调用priority
  • 调用shared_ptr的构造函数

如果在调用priority函数期间导致异常,那么“new Widget”指针将会遗失,因为它尚未置入shared_ptr内,所以就可能引发内存泄漏。来看看避免的方法:

shared_ptr<Widget> pw(new Widget);
processWidget(pw, priority());

只需要使用分离语句,分别写出创建Widget,将它置入智能指针内,然后再把智能指针传给processWidget。
所以:以独立语句将new的对象存储于只能指针内,如果不这样做,一旦异常被抛出就会产生难以察觉的内存泄漏。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值