Effective C++ 第三部分 资源管理

本文讨论了C++中资源管理的重要性,强调了对象和智能指针(如auto_ptr和shared_ptr)在管理内存和其他资源时的角色。通过RAII(Resource Acquisition Is Initialization)原则,确保资源在对象生命周期内得到正确管理。同时,文章提醒开发者注意资源管理类的复制行为,以及在使用new和delete时保持一致的形式,避免资源泄露和内存异常。此外,提供了对原始资源的访问方式,并强调了独立语句处理newed对象以增强异常安全性的必要性。
摘要由CSDN通过智能技术生成

使用的资源就必须还给系统, 常见的资源:内存, 文件描述器(file descriptors)、互斥锁(mutex locks)、网络sockets、图形界面中的字型和笔刷、数据库连接。

以对象管理资源

把资源放入对象中,这样就可以使用C++的“析构函数自动调用机制”确保资源被释放。许多资源被动态分配于heap内而后被用于单一区块或函数内,他们应该再控制流离开那个区块或函数时被释放。标准程序库提供的auto_ptr正是针对这种形式设计的特制产品, auto_ptr是一个“类指针(pointer-like)”对象, 也就是所谓的“智能指针”, 其析构函数自动对其所指对象调用delete。举例

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

两个关键想法:

1.获得资源后立即放进管理对象(managing object)内。
资源获得时机就是初始化时机(Resource Acquisition Is Initialization, RAII)
2.管理对象中运用析构函数确保资源被释放
注意:由于auto_ptr会自动删除所指之物, 所以不能多个auto_ptr指向同一个对象。因此,如果通过copy构造函数或者copy assignment操作符复制他们, 他们会变成null。

auto_ptr的替代方案, shared_ptr, “引用型计数型智能指针”(reference-counting smart pointer,RCSP),持续追踪有多少对象指向某个资源, 无人指向时自动删除。

void f( )
{
    ...
    std::trl::shared_ptr<Investment>
    pInv(createInvestment);
    ...
}

 

其中auto_ptr和shared_ptr都是在析构函数内做delete而不是delete[]动作, 所以动态分配而得的array上使用时会出现问题。

在资源管理类中小心copying行为

假设我们建立自己的资源管理类,例如,使用C API函数处理类型为Mutex的互斥器对象(mutex objects), 共有lock和unlock两函数可用:

void lock(mutex* pm);
void unlock(mutex* pm);

为确保不会忘记将一个锁住的mutex解锁, 需要建立一个class来管理锁, 这样类的基本结构由RAII守则支配。

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

客户对Lock的用法

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

但是当Lock对象被复制时

Lock ml1(&m);  //锁定m
Lock ml2(ml1);

当一个RAII对象被复制时,会产生什么后果:

1.禁止复制
许多时候允许RAII对象被复制并不合理,因为很少能够合理拥有“同步化基础器物”(synchronization primitives)的复件。如果复制动作对RAII class不合理,就应该禁止之。将copying声明为private

class Lock: private Uncopyable{
public:
     ...
};

2.对底层资源祭出”引用计数法“
通常只要内含一个trl::shared_ptr成员变量,RAII classes便可以实现出reference-counting copying行为。然而将Mutex*改变为trl::shared_ptr < Mutex >,然而我们希望的是解除锁定而不是删除, 所以我们使用shared_ptr调用指定的所谓”删除器“(deleter), 即一个函数或者函数对象(function object),删除器对shared_ptr构造函数而言是可有可无的第二参数,实例

class Lock {
public:
     explicit Lock(Mutex* pm)
        : mutexPtr(pm, unlock)
        {
            lock(mutexPtr.get());
        }
private:
     std::trl::shared_ptr<Mutex> mutexPtr;
}

本例的Lock class不再声明析构函数, 没有必要。mutexPtr的析构函数会在引用次数为0时自动调用trl::shared_ptr的删除器

复制底部资源
复制资源管理对象(当不需要某个复件时确保它被释放)时, 应该复制其所包裹的资源,复制资源管理对象, 进行的"深度拷贝"。

转移底部资源的拥有权
某些罕见场合可能确保一个RAII对象指向一个未加工资源(raw resource),即使RAII对象被复制时依然如此。此时资源的所有权会从被复制物转移到目标物, 这也是auto_ptr的复制意义。(同一个对象只能有一个auto_ptr指向,其它的都变为null)

复制RAII对象必须一并复制它所管理的资源,所以资源的copying行为决定RAII对象的copying行为

==普遍常见的RAII class copying行为时:一直copying、实行引用计数法(reference counting)。不过其它行为也都可能被实现。

在资源管理类中提供对原始资源的访问

避免资源泄露是良好设计系统的根本性质

举例

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

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

int daysHeld(const Investment* pi); //返回投资天数
int days = daysHeld(pInv); //错误
int days = daysHeld(pInv.get()); //将pInv的内部原始指针传给daysHeld

原因,daysHeld需要的是Investment*指针, 两个做法, 显示转换或者隐式转换。shared_ptr和auto_ptr都可以通过get()函数来执行显式转换。

shared_ptr和auto_ptr都重载了指针取值操作符(pointer dereferenceing),(->, *)。

class Investment{ //Investment继承体系的根类
public:
   bool isTaxFree() const;
   ...
};
Investment* createInvestment(); //工厂函数
std::trl::shared_ptr<Investment>; //令trl::shared::ptr
  pil(createInvestment( ));  //管理一笔资源
bool taxable1 = !(pil->isTaxFree()); //经由operator->访问资源
...
std::auto_ptr<Investment> pi2(createInvestment()); //领auto_ptr管理资源
bool taxable2 = !((*pi2).isTaxFree());//经由operator*访问资源

隐式转换

FontHandle getFont(); //暂时忽略参数
void releaseFont(FontHandle fh); //来自同一组C API
class Font{  //RAII class
public:
   explicit Font(FontHandle fh) //获得资源
       : f(fh)                  //pass-by-value
   { }                          //因为C这样做       
   ~Font( ) { releaseFont(f ); }  //释放资源
private:
   FontHandle f;                 //原始(raw)字体资源
};

假设大量与字体相关的C API, 他们处理的是FontHandle, 那么将Font转换为FontHandle是一种非常频繁的需求。Font函数可以提供显示的转换函数:

class Font {
public:
   ...
   FontHandle get() const { return f; } //显式转换函数
   ...
};

然而到处使用显示转换可能比较繁琐, 不愿意被人使用,从而造成资源泄露的风险(Font就是被设计用于防止资源泄露)。
因此提供隐式转换:

class Font {
public:
   ...
   operator FontHandle() const   //隐式转换函数
   {return f;}
   ...
};

这样客户调用C API时轻松且自然:

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

但是可能会增加错误发生的机会

Font f1(getFont( ));
...
FontHandle f2 = f1;  //原意拷贝一个Font对象, 但是f1被隐式转换为FontHandle才被复制。

APIs往往要求访问原始资源(raw resources), 所以每一个RAII class应该提供一个“取得所管理资源”的办法。

对原始资源的访问可能是通过显示转换或者隐式转换, 显示转换比较安全, 隐式转换对客户访问比较方便。

成对使用new和delete时要采取相同形式

如下所示:

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

程序行为不明确,stringArray中的100个对象中的99个不太可能被适当删除,因为他们的析构函数可能没有被调用。

当使用new(使用new动态生成一个对象),有两件事情发生, 第一,内存被分配出来。第二, 针对此内存会有一个(或更多)构造函数被调用。当使用delete时,内存中先有一个或多个析构函数被调用, 然后内存被释放。delete的最大问题在于,将要删除的内存中有多少对象,或者说,即将被删除的那个指针, 所指的是单一对象还是对象数组。。数组所用的内存通常还包括“数组大小”的记录, 以便delete知道需要调用多少次析构函数。单一对象的内存则没有次记录。

==当使用delete时加上“[]”, delete便认定指针指向一个数组,否则它便认定指针指向单一对象。

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

对stringPtr1使用“delete []”格式, 未定义,delete可能调用多次析构函数,删除一个并不存在的对象。
对stringPtr2使用“delete []”格式, 未定义,可能过少的析构函数被调用

当调用new时使用[], 则调用delete时也使用[], 当调用new时没有使用[], 那么调用delete时也不适用[]。

当以new创造某种typedef对象时, 应当说明以哪一种delete删除之,如下所示:

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

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

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

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

delete pal;    //行为没有定义
delete [ ] pal;//很好

为避免诸如此类的错误, 尽量不要对数组形式做typedef动作

以独立语句将newed对象置入智能指针

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

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

但是如果如此调用processWidget:

processWidget(new Widget, priority());

上述调用将无法通过编译,trl::shared_ptr的构造函数需要一个原始指针, 但是该构造函数是一个explicit函数,无法进行隐式转换,用如下形式书写可以通过编译:

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

当编译器产出一个processWidget调用码之前,必须首先核算即将被传递的各个实参。编译器创建代码,做以下三件事:

  • 调用priority
  • 执行“new Widget”
  • 调用trl::shared_ptr构造函数
    c++编译器完成这些时间的先后次序弹性很大(Java或者C#就有确定的次序)。“new Widget”肯定先于trl::shared_ptr构造函数被调用之前, 但是对priority的调用顺序可以随意。当priority被编译器选择在第二位进行执行时:
  1. 执行“new Widget”
  2. 调用priority
  3. 调用trl::shared_ptr构造函数
    当priority调用异常时,"new Widget"返回的指针会遗失, 因为它还没有被植入trl::shared_ptr中, 后者则是用来防止资源泄露的, 因此上文的调用形式虽然能够通过编译,但是在"资源被创建"(经由"new Widget")和"资源被转换为资源管理对象"两个时间蒂娜之间有可能发生异常干扰。

解决方法:分离语句
使用分离语句,先将其置入一个智能指针内, 然后再把那个智能指针传给processWidget:

std::shared_ptr<Widget> pw(new Widget); //在单独语句以智能指针存储
processWidget(pw, priority()); //这个调用动作不至于会造成泄露

此操作不会让编译器有重新排列各项操作的自由度。所以我们需要用独立语句将newed对象存储于智能指针内, 如果不这样做, 一旦异常被抛出, 有可能导致难以察觉的资源泄露。

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值