资源管理

13. 以对象管理资源

  • 当你通过工厂函数生成一个指针的时候(如下),调用者应该要对其进行delete操作。但是往往函数调用还么有到delete就由于return等操作终止了,单纯依赖f()导致无法进行到delete操作。
Investment* createInvestment();

void f(){
   
	Investment* PInv=  createInvestment();
	...
	delete PInv;
}
  • 我们可以将资源放进对象内,当控制流离开f(),该对象的析构函数会自动释放这些资源。标准库中提供的auto_ptr正是为这种形式而设计的。
void f(){
   
std::auto_ptr<Investment> pInv(createInvestment());
.....  //调用factory函数获得指针,程序结束pInv消亡,自动调用析构函数删除pINV。
}
  • 这里主要两个思想:1.获得资源后立刻放入管理对象中,即资源取得时机便是初始化时机(RAII)。2. 管理对象运用析构函数确保资源被释放。
  • auto_ptr删除对象后会调用析构删除保存的指针,并且他可以通过copy构造和赋值操作符复制,复制后原有的会变成null,复制后的指针获得位移控制权(控制权转移)。所以避免让多个auto_ptr指向同一对象。
auto_ptr<Investment> pInv1(createInvestment());  //创建智能指针
auto_ptr<Investment> pInv2(pInv1);				//转移控制权 pInv1变为null,
pInv1 = pInv2;									//同理,pInv2变为null
  • auto_ptr还有个问题就是,当对一个指针设置两个auto_ptr来管理时,当程序结束,会出现重复调用析构的问题。
int* a = new int(5);
auto_ptr<int> pInv1(a);
auto_ptr<int> pInv2(a);    //程序结束会报错,重复释放堆内存。
  • 这意味着auto_ptr并非管理动态分配内存的神兵利器,**shared_ptr(引用计数型智能指针)**会解决这个问题。但是shared_ptr也会存在循环引用的问题。通过拷贝和赋值时会让相应的计数器加1。当计数器为0是调用析构函数。值得注意的是,auto_ptr和shared_ptr内部都是调用delete函数而非delete [],所以对于array使用这两个是个馊主意。 (也可以直接让工厂函数返回一个智能指针类型,从而解决这个问题。)
  1. 为防止资源泄漏,请使用RAII(资源取得就初始化)对象,他们在构造函数中获得资源并且在析构函数中释放。
  2. 两个常被使用的RAII classes分别为shared_ptr和auto_ptr。前者通常为较佳选择,因为其中的copy行为比较直观。若选择auto_ptr,复制动作会使它(被复制物)指向null。

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

-假设定义一个类型为Mutex的互斥器对象,有lock和unlock两个函数可用:

class Lock{
   
public:
	explicit Lock(Mutex* pm):mutexPtr(pm){
   
		lock(mutexPtr);    //锁住资源
	}
	~Lock(){
   
	unlock(mutexPtr);   //释放资源
	}
private:
	Mutex* mutexPtr;
};

客户合理的用法如下:

Mutex m; //定义一个互斥器
...

{
   
Lock m1(&m); //锁住m
...
}  //结束自动释放m

上述操作时正常的,但是如果你对Lock 进行复制会发生什么?

Lock m1(&m);
Lock m2(m1);

所以你需要考虑一个RAII对象被复制时会发生什么的问题:

  1. 禁止复制。 将copying操作定义为private。对Lock而言看起来挺合适。
  2. 对底层资源祭出“引用计数器”。如同shared_ptr操作。幸运的是shared_ptr可以指定自己的删除器,所以你完全可以修改上述的代码
class Lock{
   
public:
	explicit Lock(Mutex* pm):mutexPtr(pm, unlock()){
      //智能指针管理pm,并且pm计数器为0后,
														//mutexPtr对pm调用unlock删除器。
		lock(mutexPtr);    //锁住资源
	}
	
private:
	shared_ptrL<Mutex> mutexPtr;
};
  1. 复制底部资源。使用深度拷贝,拷贝出一个附件,避免重复释放同一个空间的问题。
  2. 转移底部资源控制权。如同auto_ptr操作。
  • 值得注意的是,copying函数编译器会自动生成,如果你想要的和编译器生成的效果不同,最好自己重写定义他。

复制RAII对象必须一并复制他们所管理的资源,所以资源的copying行为决定RAII对象的copying行为。
普遍而常见的RAII类copying行为是:抑制copying、施行引用计数器。不过其他行为也可能被实现(底层复制资源与控制权转移(如auto_ptr))。

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

  • 以shared_ptr为例,当你需要一个函数将其对象转换为其所内含的原始资源时候,有两种方法可以进行。

  • 方法一:显示转换。 shared_ptr提供了一个get成员函数,他会返回智能指针内部的原始指针的复件。并且所有的智能指针都重载了->和*操作符,允许隐式转换至底部原始指针。

  • 方法二:“隐式转换函数”。操作如下:

class Font{
   
public:
	...
	Fon他Handle get()const{
    return f;} //显式转换函数
	operator FontHandle() const //隐式转换函数
	{
   
	return f;
	}
private:
	FontHandle f;
};

此时调用就方便多了:

Font f(getFont()); //获得一个智能管理资源Font 对象
int newFontSize;
...
changeFontSize(f,newFontSize)//changeFontSize需要传入FontHandle对象,这里Font隐式转换为FontHandle

但是这样也会增加错误发生:

Font f1(getFont()); //获得一个智能管理资源Font 对象
...
FontHandle f2 = f1; //原意想要对Font 对象内容进行拷贝,但是这样f1隐式转换后对其进行复制。

上述操作会导致f1被销毁后,f2就被吊空了。所以通常显示的转换更安全。

APIs往往要求访问原始资源,所以对每一个RAII 类应该提供一个取得其所管理资源的方法。(shared_ptr中比如get()函数并且他也有重载->和*)
对原始资源的访问可能经由显式转换或隐式转换。一般而言显式转换比较安全,但隐式转换对客户比较方便。

16. 成对使用new和delete时要采用相同的形式

  • 对于new,当你使用它时,他会做两件事,一是通过operator new函数分配内存,而是针对次内存会有一个或者多个构造函数被调用。
  • 当你使用delete时,针对该内存会有一个或者多个析构函数被调用,然后内存才会被(operator delete操作)释放。
  • 对于数组,相较于单一对象,在头部多了一个数组大小的记录。当你对一个指针使用delete时候,唯一能告诉他是否为数组的只有你。当你加上[],他就会知道该指针是一个数组,并且释放前会读取数组大小,来决定析构函数调用的次数。
  • 如果你对一个单一对象使用delete[],他就会读取若干内存解释为数组大小,开始多次调用析构,完全不在意所处理的内存是否为那个对象。
  • 对于一个数组只使用delete,那他则认为他是单一对象,只调用一次析构,只对数组的首位元素进行析构,后续的内容则会造成内存的泄漏。
  • 最好不要对数组形式做typedefs动作,否则还要查明其是否为数组,决定使用哪种delete。

如果你在new表达式中使用[],必须在相应的delete表达式中也使用[],如果你在new表达式中不使用[],一定不要再相应的delete表达式中使用。

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

  • 考虑以下函数:
int priority();
void processWidget(std::trl::shared_ptr<Widget> pw,int priority);

进行如下调用:

processWidget(new Widget,priority());

但是shared_ptr的构造函数是一个explicit,无法进行隐式转换,无法把一个Widget指针转为一个shared_ptr类型,所以该调用会报错。

processWidget(shared_ptr<Widget>(new Widget)priority()); //成功

有三件事要做:

  1. priority()
  2. new Widget
  3. shared_ptr() 构造函数

在VS编译环境,我测试过是从右到左。但是不同的编译环境弹性很大,如果执行顺序变换后变为2-1-3.那么如果在执行好申请内存后,priority()出现异常,导致shared_ptr() 构造函数么有进行执行,那么申请的内存没有得到管理,这就会导致内存的泄漏。

  • 所以尽量将1和23分离开,先执行2,3,确保得到管理后,在和1一起执行。
shared_ptr<Widget> pw(new Widget);
processWidget(pw,priority()); //成功,不会考虑异常出现会导致内存泄漏的问题。编译器可以自行选择执行顺序。

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