Effective C++ ——资源管理

条款13:以对象来管理资源


       在C++中我们经常会涉及到资源的申请与申请,一般都是由关键字new 和 delete来操作的,两者都是成对存在的,缺一不可,否则会出现意想不到的问题,例如:

class Investment{.....};
Investment* pinv = createInvestment();
       我们在使用完后要动态的释放掉pinv所指向的资源,例如在下面的函数中做了调用:

void f(){
    Investment* pinv = createInvestment();
    ...
    delete pinv;
}
       正常情况下这将运行良好,但是如果在...中函数提前的返回了或者在跟特别的出现了异常程序异常的停止了,这是delete函数将
得不到执行,此时就会出现传说中的内存泄露问题。
       为了对这种情况进行处理,我们可以采用一个专门的类来对这里的资源进行处理,在这个类的析构函数中处理对资源的释放工作,这样当对象离开作用空间的时候会自动的调用其析构函数,资源也会自动的得到释放,在STL中,有一个专门的类来处理这中情况auto_ptr,也就是传说中的”智能指针“;我们可以这么用:
std::auto_ptr<Investment> pivn = createInvestment();
       这样当函数f执行结束的时候,auto_ptr离开它的作用域,将会自动的调用auto_ptr的析构函数,此时pinv所指向的资源也就得到了释放!
       需要注意的是:auto_ptr智能指针是独占的,也就是不能有同样的auto_ptr指向同样的资源。由于auto_ptr被销毁时会自动删除它所指之物,所以一定要注意别让多个auto_ptr同时指向同一对象,例如:

std::auto_ptr<Investment> pivn1(createInvestment());//pivn1指向createInvestment返回值
std::auto_ptr<Investment> pivn2(pivn1); //pivn2指向对象,pivn1被设置为NULL
pivn1 = pivn2; ////pivn1指向对象,pivn2被设置为NULL
      auto_ptr有个不寻常的性质:若通过拷贝构造或赋值操作符复制它们,它们会变成null,而复制所得的指针将取得资源的唯一拥有权。
       相对应的tr1::shared_ptr就能解决这种问题,他是通过引用计数来对资源进行释放的,当多个shared_ptr指向同样的资源的时候这个资源的引用计数也会随之增加,当资源的引用计数变为0的时候,资源会被析构,这样在一些通过资源的复制而实现的结构中可以采用shared_ptr,例如vector等。
       不过shared_ptr对下面这两种情况无能为力:

1.对于动态分配的array上是不能使用的,因为shared_ptr内部采用的是delete而不delete[]; 

2. 对于环状引用的情况也是不能处理的,此时应该采用weaked_ptr;
       总之如果你的程序中有delete相关操作的出现,那就说明你的程序有随时出现意想不到情况出现的可能!

请记住:

  • 为防止内存泄露,请使用RAII,他在构造函数中获得资源,在析构函数中释放资源。
  • 两种常用的RAII是指的:auto_ptr和shared_ptr,后者是最佳的选择,因为它能很好的处理copy操作,前者是独占的!

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


       在条款中主要的介绍了智能指针的用法,那里解决为问题是指向一个heap空间的指针的申请与释放问题,然而并非所有的资源都是在heap中申请的,这时候智能指针就不适合了,例如对于类型为Mutex的互斥器对象,只有lock和unlock的操作,在这里lock与unlock是成对存在的,为了防止调用lock后忘记unlock我们可以自己管理资源,例如:

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

Mutex m;
.....
{
    ......
    Lock m1(&m)
    ......
}
       这样在m1离开作用域的时候,会自动的调用Lock的析构函数也就是Mutex的unlock函数解锁!
       但是Lock对象被复制,会发生什么事?

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

此时会怎样来处理?这个主要有以下几种方法:
1.禁止复制,有时候有些对象是不适合被复制的,对于一个想Lock这样的对象就是这样的情况,我们可以采用前面介绍的方法,

class Lock:private Uncopyable{ //禁止复制
public:
    ....
};


2.对底层资源采用"引用计数法",tr1::shared_ptr就是这种情况,当资源的被复制的时候,资源的引用计数就会+1,对应的如果引用计数为0,就释放该资源,shared_ptr缺省的情况下会在指针计数为0 的时候释放资源,在特殊情况下我们可以制定指针为0的时候采用的动作,例如:
class Lock{
public:
	explicit Lock(Mutex* pm):mutexPtr(pm,unlock){
		lock(mutexPtr.get());
	}
private:
	tr1::shared_ptr<Mutex> mutexPtr;
};
在本例中不用再写析构函数,因为默认析构函数调用的时候,会自动的调用mutexPtr的析构函数,即为前面制定的unlock函数。


3.复制底部资源。只要你喜欢你可以对一个申请的资源做任何多份的copy,此时copy的时候不仅要copy资源管理类,还要对其包裹的任何的资源进行复制,这就是所谓的深度copy。


4.移交底部资源的所有权。例如auto_ptr,每份资源只有一个资源管理对象,当copy的时候,被copy的资源将变为空。

请记住:

  • 复制RAII对象必须一并复制它所管理的资源,所以资源的copying行为决定RAII的行为
  • 常见的RAII类的copy行为主要有:禁止copying,采用引用计数法,还有上面介绍的两种也是常用的方法!

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

       前面几个条款主要的将了资源管理类对资源的管理,但是如果需要资源管理类的原始资源的时候该怎么做呢?例如:

class Investment{
public:
    bool isTexFree() const;
    ...
};
std::tr1::shared_ptr<Investment> pinv = createInvestment();
       此时有个函数调用:
void dayHeld(Investment* ph);
       此时如果直接用dayHeld(pinv)是错误的,因为要求是指针!
       在shared_ptr和auto_ptr等系统的智能指针资源管理类中,存在一个get()的函数,可以获得对应的原始资源例如:

void dayHeld(pinv.get());

       此外在几乎所用的智能指针中几乎都对*和->操作符做了重载,例如:

bool isTrue = pinv->isTextFree();
bool isFalse = (*pinv).isTextFree();
       为了兼容性,我们一般在自己的资源管理类中也会定义get()函数来获得对原始资源的调用,例如:
class Font{
public:
	explicit Font(FontHandle fn):f(fn){}
	....
	FontHandle get() const{
		return f;
	}
	...
	~Font(){realseFont(f);}
private:
	FontHandle f;
};
       上面定义的Font类可以看做是FontHandle类的资源管理类,对于需要FontHandle类型的函数调用,我们可以通过Font类的get()函数获得,与智能指针的用法几乎相同。此外还可以用隐式类型转化来替换get()函数调用,这种用法容易出现问题,建议不要使用

请记住:

  • APIs往往需要取得RAII的原始资源,因此对于RAII都要提供一种对原始资源的访问,就像get()函数
  • 对原始资源的访问有隐式和显式两种,我们这里只介绍了显式类型转化,对于隐式类型转化应用比较少

条款16:当成对的使用new 和 delete时,要确保new 和 delete的格式是相同的


       这个条款比较简单,但是却很容易出错,首先我们看下当我们使用new和delete的时候编译器为我们做了什么,当我们调用new操作符的时候,编译器会首先在内存中帮我们申请一块空间,然后调用对应对象的构造函数,想对应的当我们调用delete的时候,编译器会首先在该空间调用对应的析构函数然后再对该空间资源进行释放,例如:

std::string* p = new string[10];

       对于上面的资源申请,我们释放的时候要对应的采用:

delete [] p;
       如果我们采用了delete p的形式,将会出现不确定的结果,我们是通过[]符号来告诉编译器要析构的是一个对象还是一个对象的列表,对应的会调用一次析构函数或者多次析构函数,如果在单一对象上调用多次析构函数或者在多个对象上调用单次析构函数,后果可想而知!
       在使用中我们需要注意的一种情况是typedef,例如:
typedef std::string AddressLine[10];
std::string* p = new AddressLine;
       此时我们在调用delete的时候一定要注意使用delelet[], 在C++中存在强大的容器类,如果应用恰当完全可以将C中引入的array数组替代掉,例如上面我们完全可以采用vector<string>的形式,这样就不用担心资源的释放问题了!

请记住:

  • 对于资源的申请和释放调用new和delelte一定要采用相同的形式,new 对应delete , new[] 对应delete[]!

条款17:以独立的语句将new的资源放入到智能指针中


       考虑以下情况:

int prirority();
void processWidget(std::shared_ptr<Widget> pw, int priority);
       我们在对processWidget函数进行调用的时候,可以采用如下形式:
processWidget(std::tr1::shared_ptr<widget> pw(new widget), priority());
       注意我们不能直接将new widget作为实参传入std:shared_ptr<widget> pw形参中,在上面的函数调用中,看起来没有问题但是可能回出现内存泄露的情况,因为在函数中,参数的调用顺序会因为编译器的不同而不同的,例如上面的循序可能是:
  • new widget
  • priority调用
  • std::tr1::shared_ptr<widget> 初始化
       这样可能出现的问题就是当new widget成功后,如果priority()函数调用失败,由于new widget未能放入到智能指针中,但是退出的时候资源得不到释放,解决办法就是讲实参独立出来,例如:

std::tr1::shared_ptr<widget> pw(new widget);
int pri = priority();
processWidget(pw,pri);

请记住:

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

posted on 2013-08-24 18:54  胡永光  阅读(158)  评论(0编辑  收藏  举报

导航