第三章、资源管理

所谓资源就是,一旦用了它,将来必须还给系统。如果不这样,糟糕的事情就会发生。C++程序中最常使用的资源就是动态分配内存(如果你分配内存却从来都不曾归还它,会导致内存泄漏),但内存只是你必须管理的众多资源之一。其它常见的资源包括文件描述器、互斥锁、图形界面中的字型和笔刷、数据库连接、以及网络sockets。无论哪一种资源,当你不再使用它,必须将它还给系统。

条款13:以对象管理资源

假设有一个Investment类,有一个工厂函数——createInvestment产生特定的Investment对象:

class Investment{};//"投资类型"继承体系中的root class
Investment* createInvestment();//返回指针,指向Investment继承体系内的动态分配对象。
//调用者有责任删除它。这里为了简化,刻意不写参数

f函数调用了createInvestment函数返回对象后,有责任删除之。

void f(){
    Investment *pInv=createInvestment();
    ...
    delete pInv;    //释放pInv所指对象。
}

f函数可能在”…“区域内的一个过早的return语句,造成控制流不会触及delete语句,从而造成内存泄漏。

下面示范如何使用auto_ptr以避免f函数潜在的资源泄漏可能性:

void f(){
    std::auto_ptr。<Investment> pInv(createInvestment());
    //调用factory函数,一如既往地使用pInv,经由auto_ptr的析构函数自动删除pInv;
    ...     
}

这个简单例子示范”以对象管理资源“的两个关键想法:
1、获得资源后立刻放进管理对象内。
2、管理对象运用析构函数确保资源被释放。

由于auto_ptr被销毁时会自动删除它所指之物,所以一定要注意别让多个auto_ptr同时指向同一个对象。如果真是这样,对象会被删除一次以上,而那会使你的程序出现”未定义行为“。为了预防这个问题,auto_ptrs有个不寻常的性质:若通过copy构造函数或copy assignment操作符复制它们,它们会变成null,而复制所得指针将取得资源的唯一拥有权!

std::auto_ptr<Investment> pInv1(createInvestment());//pInv1指向createInvestment返回值
std::auto_ptr<Investment> pInv2(pInv1);//现在pInv2指向对象,pInv1被设为null
pInv1=pInv2;//现在pInv1指向对象,pInv2被设为null

”受auto_ptr管理的资源必须绝对没有一个以上的auto_ptr同时指向它“。因此,替代方案是”引用计数型智慧指针(reference-counting smart pointer,RCSP )“,也就是shared_ptr。

void f(){
    shared_ptr<Investment> pInv1(createInvestment());//pInv1指向createInvestment返回值
    shared_ptr<Investment> pInv2(pInv1);//pInv1和pInv2指向同一个对象
    pInv1 = pInv2; //同上,无任何改变
    ...
}//pInv1和pInv2被销毁,它们所指的对象也就被自动销毁

auto_ptr 和shared_ptr两者都在其析构函数内做delete而不是delete[ ]动作。那就意味着动态分配而得的array身上使用auto_ptr或shared_ptr是个馊主意。尽管如此,那么做还能通过编译:

auto_ptr<string> aps(new string[]);//馊主意! 会用上错误的delete形式
shared_ptr<int > spi(new [1024]);//相同问题

createInvestment返回的”未加工指针“(new pointer)简直是对资源泄漏的一个死亡邀约,因为调用者极易在这个指针身上忘记delete。

请记住:
1、为了防止资源泄漏,请使用RAII(资源取得时机便是初始化时机)对象,它们在构造函数中获得资源并在析构函数中释放资源。
2、两个常被使用的RAII classes分别是tr1::shared_ptr和auto_ptr。前者通常是较佳选择,因为其copy行为比较直观。若选择auto_ptr,复制动作会使它(被复制物)指向null。

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

假设我们使用C API函数处理类型为Mutex的互斥器对象,共有lock和unlock两个函数可用:

void lock(Mutex *pm);//锁定pm所指的互斥器
void unlock(Mutex *pm);//将互斥器解除锁定

为了确保绝不会忘记将一个被锁住的Mutex解锁,你可能会希望建立一个class用来管理锁。这样的class的基本结构由RAII守则支配,也就是“资源在构造期间获得,在析构期间释放”:

#include <mutex>   
using namespace std;
typedef  mutex Mutex;
class Lock{
public:
    explicit Lock(Mutex* pm):mutexPtr(pm){
             lock(mutexPtr);                       
        }
    
    ~Lock(){ unlock(mutexPtr);}//释放资源
private:
    Mutex *mutexPtr;  
    
    void lock(Mutex *pm);//锁定pm所指的互斥器
    void unlock(Mutex *pm);//将互斥器解除锁定
};

客户对Lock的用法符合RAII方式:

Mutex  m;//定义你需要的互斥器
...
{               //建立一个区块用来定义critical section
	Lock ml(&m);//锁定互斥器
      ...              //执行critical section内的操作
}               //在区块最末尾,自动解除互斥器锁定

//这很好,但如果Lock对象被复制,会发生什么事情?

Lock ml1(&m);//锁定m
Lock ml2(ml1);//将ml1复制到ml2身上,这会发生什么事情?

这是一个一般化问题的特定例子。那个一般化问题是每一位RAII class作者一定需要面对的:“当一个RAII对象被复制,会发生什么事?”大多数时候你会选择以下两种可能:
1、禁止复制。
就是“将copying操作声明为private”。对Lock而言看起来是这样的:

class Lock:private Uncopyable{
public:
    ... //如前
};

2、 对底层资源祭出“引用计数法”。例子就是使用shared_ptr。

**复制底部资源。**也就是说,复制资源管理对象时,进行的是“深度拷贝”

**转移底部资源的拥有权。**某些罕见场合下你可能希望确保永远只有一个RAII对象指向一个未加工资源。即使RAII对象被复制依然如此。此时资源的拥有权会从被复制物转移到目标物。auto_ptr奉行这种复制意义。

请记住:
1、复制RAII对象必须一并复制它所管理的资源,所以资源的copying行为决定RAII对象的copying行为。
2、普通而常见的RAII class copying行为是:抑制copying、施行引用计数法。不过其他行为也都可能被实现。

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

使用智能指针如auto_ptr或者shared_ptr保存factory函数如createInvestment的调用结果:

std::shared_ptr<Investment> pInv(createInvestment());//

假设你希望以某个函数处理Investment对象,像这样:

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

你需要这么调用它:

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

却不通过编译,因为daysHeld需要的是Investment*指针,你传给它的却是个类型为std::shared_ptr 的对象。此时你需要一个函数可以将RAII class对象转换为其所内含之原始资源。两种方法:显式转换和隐式转换。

shared_ptr和auto_ptr都提供一个get成员函数,用来执行显式转换,也就是它会返回智能指针内部的原始指针(的复件):

 int days=daysHeld(pInv.get());//很好,将pInv内的原始指针传给daysHeld

就像(几乎)所有智能指针一样,shared_ptr和 auto_ptr也重载了指针取值操作符(operator->和operato*),它们允许隐式转换至底部原始指针:


#include <iostream>
#include<memory>
using namespace std;

class Investment{//investment继承体系的根类
public:
       bool isTaxFree() const;
       ...
};
Investment * createInvestment(){
    ...
}

int main()
{
    Investment * createInvestment();//factory函数
    std::shared_ptr<Investment> pi1(createInvestment());//令shared_ptr管理一笔资源
    
    bool texable1 =!(pi1->isTaxFree());//经由operator->访问资源。
    ...
    std::auto_ptr<Investment> pi2(createInvestment());//令auto_ptr管理一笔资源
    bool texable2 =!((*pi2).isTaxFree());//经由operator->访问资源
    ...
    
    
    return 0;
}

下面这个用于字体的RAII class(对于C API而言字体是一种原生数据结构)

FontHandle getFont();//这是个C API。为求简化暂略参数
void releaseFont(FontHandle  fh);//来自同一组 C API

class Font{     //RAII class
public:
    explicit Font(FontHandle fh):f(fh){//获得资源
        //采用pass-by-value
    }
    ~Font(){releaseFont(f)}//释放资源
  
private:
    FontHandle f;       //原始(raw)字体资源
};

下面对Font class添加隐式转换和显式转换函数。

class Font{     //RAII class
public:
    explicit Font(FontHandle fh):f(fh){//获得资源
        //采用pass-by-value
    }
    ~Font(){releaseFont(f)}//释放资源
  
    FontHandle get() const {return f;}//显式转换函数
    operator FontHandle() const//隐式转换函数
    {
        return f;
    }
private:
    FontHandle f;       //原始(raw)字体资源
};

显式用法如下

    Font f(getFont());
    int newFontSize;
    ...
    changeFontSize(f.get(),newFontSize);//明白地将Font转换为FontSize

这种显式转换,使用麻烦,存在增加泄漏字体的可能性,而Font class的主要设计目的就是为了防止资源泄漏。

下面是隐式转换的方式

    Font f(getFont());
    int newFontSize;
    ...
    changeFontSize(f,newFontSize);//将Font隐式转换为FontHandle

隐式转换的方式使用比较方便,但是会增加错误发生机会。例如客户可能会在需要Font时意外创建一个FontHandle:

    Font f1(getFont());
    int newFontSize;
    ...
    FontHandle f2=f1;//原意是要拷贝一个Font对象,
    //却反而将f1隐式转换为其底部的FontHandle然后才复制它

以上的FontHandle由Font对象f1管理,但那个FontHandle也可以通过直接使用f2取得。那几乎不会有好结果。例如当f1被销毁,字体被释放,而f2因此成为“虚吊的(dangle)”。
通常显式转换函数如get是比较受欢迎的方法,因为它将“非故意之类型转换”的可能性最小化了。然而有时候,隐式类型转换所带来的“自然用法“也会引发天枰倾斜。

请记住:
1、APIs往往要求访问原始资源,所以每一个RAII class应该提供一个“取得其所管理之资源”的ff

2、对原始资源的访问可能经由显式转换或者隐式转换。一般而言显式转换比较安全,但隐式转换对客户比较方便。

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

当你使用new(也就是通过new动态生成一个对象),有两件事发生。第一,内存被分配出来(通过名为operator new的函数)。第二,针对此内存会有一个(或多个)构造函数被调用。当你使用delete,也有两件事发生:针对此内存会有一个(或多个)析构函数被调用,然后内存才被释放(通过名为operator delete的函数)。delete的最大问题在于:即将被删除的内存之内究竟存有多少个对象?这个问题的答案决定了有多少个析构函数必须被调用起来。

    string* stringPtr1 = new string;
    string* stringPtr2 = new string[100];
    ...
    
    delete stringPtr1;  //删除一个对象
    delete [ ] stringPtr2;  //删除一个由对象组成的数组

如果你在调用new时使用[ ],你必须在对应调用delete 时也使用[ ] 。如果你调用new 时没有使用[ ],那么也不该在对应调用delete时使用[ ]。

当程序员以new 创建该种typedef 类型对象时,该以哪一种delete形式删除之。考虑下面这个typedef:

typedef  string AddressLines[4];//每个人的地址有4行,每行是一个string
   //由于AddressLines是个数组,如果这样使用new:
    
    string *pal =new AddressLines;//注意,“new AddressLines”返回一个string*,就像“new string[4]”一样。
    
    //那就必须配匹 “数组形式的”delete:
    delete pal; // 行为未有定义
    delete [] pal ;//很好

为了避免诸如此类错误,尽量不要对数组形式做typedef动作。这很容易达成,因为STL含有string、vector等templates,可以将数组的需求降至几乎为零。例如本例的AddressLines定义为”由strings组成的一个vector“,也就是其类型为vector。

请记住:
1、如果你在new 表达式中使用[ ],必须在相应的delete表达式中也使用[ ]。如果你在new表达式中不使用[ ],一定不要在相应的delete表达式中使用[ ] 。

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

假设有个函数用来揭示处理程序的优先权,另一个函数用来在某动态分配所得的Widget上进行某些带有优先权的处理:

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

下面是调用方法:

processWidget(new Widget, priority());

这种调用方法不能通过编译。shared_ptr构造函数需要一个原始指针,但这个函数是个explicit构造函数,无法进行隐式转换,使得自”newWidget“的原始指针转换为processWidget所要求的shared_ptr。如果写成下面这样可以通过编译:

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

虽然这里使用”对象管理式资源“,但是有可能泄漏资源。
在调用processWidget之前,编译器必须创建代码,做以下三件事:
1、调用priority;
2、执行”new Widget“;
3、调用shared_ptr的构造函数。
这里的执行顺序是不确定的。可以确定的是”new Widget“一定执行于shared_ptr的构造函数调用之前,因为这个表达式的结果还要被传递作为shared_ptr构造函数的一个实参,但对priority的调用则可以排在第一或者第二或者第三。如果编译器选择priority放在第二执行,就像下面这样:
1、执行”new Widget“;
2、调用priority;
3、调用shared_ptr的构造函数。

因此,万一priority调用异常,会导致”new Widget“返回的指针遗失,因为它尚未被置入shared_ptr内,导致内存泄漏。

为了解决这个问题,使用分离语句,分别写出(1)创建Widget,(2)将它置入一个智能指针内,然后再把那个智能指针传给processWidget:

    shared_ptr<Widget> pw(new Widget);//在单独语句内以智能指针存储newed所得对象
    processWidget(pw, priority());//这个调用动作不至于造成泄漏

以上之所以行得通,因为编译器对于”跨越语句的各项操作“没有重新排列的自由(只有在语句内它才拥有那个自由度)。上面代码中,”new Widget“表达式以及“对shared_ptr构造函数的调用”这两个动作,和”对priority的调用“是分隔开来的,位于不同的语句内,所以编译器不得在它们之间任意选择执行次序。

请记住:
以独立语句将newed对象存储于(置入)智能指针内。如果不这样做,一旦异常被抛出,有可能导致难以察觉的资源泄漏。

来源:Effective C++
仅供学习,侵删

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值