Effective C++ 笔记③

第三章:资源管理

所谓资源就是,一旦用了它,将来必须还给系统;否则,糟糕的事情就会发生。C++中常用的资源就是动态分配内存,如果未能及时释放归还它,将会导致内存泄露(该块内存区域将永远不能使用)。

其他的资源还包括:

  • 文件描述器
  • 互斥锁
  • 图形界面中的字型和笔刷
  • 数据库连接
  • 网络sockets

条款13:以对象管理资源

假设我们使用一个用来塑模投资行为(例如股票、债券等)的程序库,其中各式各样的投资类型继承自一个root class Investment:

class Investment {...};    //"投资类型"继承体系中的root class

进一步假设,这个程序库系通过一个工厂函数(条款7)供应我们某特定的Investment对象:

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

现在编写一个f函数履行删除该对象的责任:

void f(){
    Investment* pInv = createInvestment();    //调用factory函数
    ...
    delete pInv;                              //释放pInv所指对象
}

以上函数似乎是可行的,但某些情况下并不能很好的执行,因为"..."内可能存在return语句导致过早的返回,未能成功执行delete语句;另一可能就是"..."内可能会抛出异常,则同样无法执行delete语句。因此,我们泄露的不只是内含投资对象的那块内存,还包括那些投资对象所保存的任何资源。

未确保资源总是被释放,我们需要将资源放进对象内,当控制流离开f,该对象的析构函数会自动释放那些资源;即把资源放进对象内,依赖于C++的“析构函数自动调用机制”确保资源被释放。

标准库函数提供的auto_ptr正是针对这种形势设计的产品。它是个"类指针(pointer-like)对象",也就是所谓的“智能指针”,其析构函数自动对其所指对象调用delete。例:

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

以上即RAII -----> "资源取得时机便是初始化时机"。

【注】由于auto_ptr被销毁时会自动删除它所指之物,所以一定要注意别让多个auto_ptr同时指向同一对象;因为这会使得对象删除一次以上,导致“未定义的行为”。auto_ptr有一个不寻常的性质:若通过copy构造函数或拷贝分配操作符复制他们,他们会变成null,而复制所得的指针将取得资源的唯一使用权!

//pInv1指向createInvestment返回物
std::auto_ptr<Investment> pInv1(createInvestment());   

//现在的pInv2指向对象,pInv1被设为null
std::auto_ptr<Investment> pInv2(pInv1);

//现在的pInv1指向对象,pInv2被设为null
pInv1 = pInv2;

以上均说明“受auto_ptr管理的资源必须绝对没有一个以上的auto_ptr同时指向它”。

auto_ptr的替代方案是“引用计数型智慧指针”,其持续追踪共有多少对象指向某笔资源,并在雾刃指向它时自动删除该资源;行为类似于垃圾回收,典型示例是tr1::shared_ptr,即:

void f(){
    ...
    //调用factory函数
    std::tr1::shared_ptr<Investment> pInv(createInvestment());
    ..。
    //经由shared_ptr析构函数自动删除pInv
}

auto_ptr和tr1::shared_ptr两者都在其析构函数内做delete而不是delete[ ]操作;这意味着在动态分配而得的array身上使用这两个智能指针是个馊主意,但其错误仍能通过编译。

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

上述条款阐明了“资源取得时机便是初始化时机”,并以此作为“资源管理类”的脊柱,也描述了auto_ptr和tr1::shared_ptr如何将这个观念表现在heap-based资源上。然后并非所有资源都是heap-based,对那种资源而言,像这两种指针往往不适合作为资源掌管者。因此,我们需要自己建立相应的资源管理类。

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

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

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

class Lock{
public:
    explict Lock(Mutex* pm):mutexPtr(pm){
        lock(mutexPtr);                    //获得资源
    }
    ~Lock() { unlock(nutexPtr); }          //释放资源

private:
    Mutex* mutexPtr;
};

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

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

但如果Lock对象被复制,会发生什么事?

Lock m11(&m);        //锁定m
Lock m12(m11);       //将m11复制到m12身上,会发生什么事呢

对此,我们有以下两种选择:

  1. 禁止复制:将copying操作声明为private,即条款6;
  2. 对底层资源祭出“引用计数法”:直到最后一个使用者(某对象)被销毁,即类似shared_ptr智能指针;
  3. 复制底部资源:当你不再需要某个复件时确保它被释放,在此情况下复制资源管理对象,应该同时也复制其所包覆的资源;也就是进行了“深度拷贝”;
  4. 转移底部资源的拥有权:某些场合下可能希望确保永远只有一个RAII对象指向一个未加工资源,此时资源的拥有权会从被复制物转移到目标物;

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

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

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

假设希望某个函数来处理Investment对象,如:

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

我们需要这么使用:

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

以上不能通过编译,因为daysHeld需要的是Investment* 指针,而我传递的却是个类型为tr1::shared_ptr<Investment>对象;这个时候需要一个函数将RAII class对象转换为其所内含之原始资源(Investment*)。有两个做法:

  • 显示转换:

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

int days = daysHeld(pInv.get());
  • 隐式转换

就像(几乎)所有智能指针一样,这两个智能指针也重载了指针取值操作符(operator->和operator*):

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

Investment* createInvestment();  //factory函数

//令shared_ptr管理一笔资源
std::tr1::shared_ptr<Investment> pi1(createInvestment());

//经由operator->访问资源
bool taxable1 = !(pi1->isTaxFree());
...

//令auto_ptr管理一笔资源
std::auto_ptr<Investment> pi2(createInvestment());

//经由operator*访问资源
bool taxable2 = !((&pi2).isTaxFree());

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

std::string* stringArray = new std::string[100];
...
delete stringArray;

以上代码使用new创建了一个长度为100的string数组,同时也使用了delete删除该指针,但编译器仅可能删除string数组中的一个对象,其余99个不能被适当的删除,因为它们的析构函数很可能没有被调用。

当使用new创建对象,有两件事发生:

  1. 内存被分配出来,通常在堆(heap)上;
  2. 针对此内存会有一个(或更多)构造函数被调用;

当使用delete删除对象,也有两件事发生:

  1. 针对此内存会有一个(或更多)析构函数被调用;
  2. 内存释放;

实际上,都可以简化成一个问题:在删除指针的时候,所指的单一对象还是对象数组?

当你对着一个指针使用delete,唯一能够让delete知道内存中是否存在一个“数组大小记录”的办法就是:由你来告诉它。如果你使用delete时加上中括号,delete便认定指针指向一个数组,否则便指向一个单一对象。

std::string* stringPtr1 = new std::string;
std::string* stringPtr2 = new std::string[100];
...
delete stringPtr1;
delete[] stringPtr2;

【注】如果你调用new时用了[ ],你必须在对应调用delete时也使用[ ];如果new时未使用[ ],则delete时也不该使用[ ]。

对于使用typedef的人来说,当程序员以new创建该种typedef类型对象时,该以哪一种delete形式删除之。考虑以下:

typedef std::string AddressLines[4];    //每个人地址有4行,每行一个string

由于AddressLines是个数组,如果这样使用new:

std::string* pal = new AddressLines;
//注意,“new AddressLines”返回一个string*,就像“new string[4]”一样

那就必须匹配“数组模式”的delete:

delete pal;        //行为未有定义!
delete[] pal;      //正常!

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

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

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

由于“以对象管理资源”的智慧铭言,processWidget决定对其动态分配得来的Widget运用智能指针。

现在考虑调用processsWidget:

processWidget(new Widget,priority());

上述形式并不能通过编译。tr1::shared_ptr的构造函数需要一个原始指针,但该构造函数是个explicit构造函数,无法进行隐式转换,将得自“newWidget”的原始指针转换为processWidget所要求的tr1::shared_ptr。如果写成下述形式即可通过编译:

processWidget(std::tr1::shared_ptr<Widget>(new Widget),priority());

在调用

std::tr1::shared_ptr<Widget>(new Widget)

时,将有两部分构成:

  1. 执行 “new Widget” 表达式
  2. 调用tr1::shared_ptr构造函数

于是在调用processWidget之前,编译器必须创建代码,做以下三件事:

  1. 调用priority
  2. 执行“new Widget”
  3. 调用tr1::shared_ptr构造函数

但是,C++编译器以什么样的次序去完成这些事情呢?答案是未知的。目前可以确定创建对象的操作必定是第一位,但如果是按照以下顺序执行呢?

  1. 执行“new Widget”
  2. 调用priority
  3. 调用tr1::shared_ptr构造函数

万一priority函数的调用导致异常,会发生什么事呢?这个时候“new Widget”返回的指针将会遗失,未能够进入智能指针中,从而导致了资源泄露

避免这类问题的方法也很简单,使用分离语句,分别写出

  1. 创建Widget
  2. 将它置入一个智能指针内,再传入processWidget

即:

std::tr1::shared_ptr<Widget> pw(new Widget);
processWidget(pw,priority());
发布了90 篇原创文章 · 获赞 6 · 访问量 1万+

猜你喜欢

转载自blog.csdn.net/weixin_37160123/article/details/101321443