【C++】资源管理
C++中内存管理是需要程序员自己控制,系统不提供垃圾回收器,因此,我们在编程要确保动态分配的内存,必须释放,归还给系统.
然而,可能由于程序员的疏忽,或者程序中出现一些异常现象,会导致程序无法到达释放内存的语句,从而造成内存泄露.
如何才能避免这样错误,让内存资源管理简单化?
解决思路:以对象管理资源,其包含两个关键想法如下.
1.获得资源后立刻放进管理对象内.实际上"以对象管理资源"的观念常被称为"资源取得时机便是初始化时机".
2.管理对象运用析构函数确保资源被释放.不论控制流如何离开区块,一旦对象被销毁(例如对象离开作用域)其析构函数自然会被自动调用,释放资源.
下面介绍两种基本的对象管理资源的方案.auto_ptr和shared_ptr.
标准程序库提供的auto_ptr.auto_ptr是个"类指针对象",即智能指针,其析构函数自动对其所指对象调用delete.
使用auto_ptr应注意不能让多个auto_ptr同时指向一个对象.
由于auto_ptr被销毁时会自动删除它所指之物,多个auto_ptr同时指向一个对象时,会导致对象被多次删除,造成"未定义行为".
为了避免出现对象被多次删除的问题,auto_ptr提供了一个不寻常的性质.
若通过copy构造函数或copy assignment操作符复制它们,它们会变成null,而复制所得的指针将取得资源的唯一拥有权.代码解释如下:
1 class Investment {...}; 2 Investment* createInvestment(); //返回指针,指向Investment继承体系内的动态分配对象 3 4 std::auto_ptr<Investment> pInv1(createInvestment()); //pInv1指向createInvestment返回对象 5 std::auto_ptr<Investment> pInv2(pInv1); //pInv2指向对象,pInv1被设为null 6 pInv1 = pInv2; //PInv1指向对象,PInv2被设为null
auto_ptr虽然可以完成资源的管理,但它无法完成合理的,正常的复制操作.
为了解决auto_ptr方案中存在的问题,我们参用另一种方案shared_ptr方案.
引用计数型智慧指针(RCSP),持续追踪共有多少对象指向某笔资源,并在无人指向它时自动删除该资源.该方案可以提供合理,正常的复制操作.如下面代码所示:
1 std::tr1::shared_ptr<Investment> pInv1(createInvestment()); //pInv1指向createInvestment返回对象 2 std::tr1::shared_ptr<Investment> pInv2(pInv1); //pInv1和pInv2指向同一个对象 3 pInv1 = pInv2; //同上,无任何改变
RCSP也存在着一个问题:无法打破环状使用(例如两个其实已经没被使用的对象彼此互指,因而好像还处在"被使用"状态,两个对象无法被销毁).
此外,还有一个auto_ptr和tr1::shared_ptr的共同问题,两者都在其析构函数内做delete而不是delete[]动作.
因此,不应该在动态分配而得的array身上使用auto_ptr或tr1::shared_ptr.
1 //可以通过编译,但会导致运行错误 2 std::auto_ptr<std::string> aps(new std::string[10]); //会用上错误的delete形式,而不是正确的delete[] 3 std::tr1::shared_ptr<int> spi(new int[1024]); //同上
虽然罐装式的资源管理类如auto_ptr和tr1::shared_ptr可以以对象的方式管理资源,但有时候它们并不能满足我们的应用需求,这时就需要我们自己制作自己的资源管理类.
制作资源管理类,要注意哪些问题?
1.根据应用需求,完成资源管理类中的copying函数(包含copy构造函数和copy assignment操作符).
一般有以下四种复制策略可供选择:
1).禁止复制.有些时候对资源管理对象复制并不合理,复制动作应该被禁止.
例如管理互斥锁的对象,一般情况下,互斥锁是不应该被复制,因此应该禁止互斥锁管理的对象的复制行为.
具体禁止复制的方案请参考:http://www.cnblogs.com/dwdxdy/archive/2012/07/16/2594113.html
2).对底层资源用"引用计数法".shared_ptr参用的是这种复制策略.
3).复制底部资源.复制资源管理对象时,进行的是"深度拷贝".
例如字符串对象内含一个指针指向一块heap内存块,当这样一个字符串对象被复制,不论指针或其所指针内存都会被制作出一个复件.
4).转移底部资源的拥有权.auto_ptr参用的是这种复制策略.
某些罕见场合下可能希望确保永远只有一个资源管理对象指向一个未加工资源,即使资源管理对象被复制.此时资源的拥有权会从被复制转移到目标物.
2.在资源管理类中提供对原始资源的访问.
我们需要资源管理类中,提供一个函数,可以将资源管理对象转换成其所内含的原始资源.具体做法可参auto_ptr和tr1::shared_ptr的实现.
auto_ptr和tr1::shared_ptr都提供一个get成员函数,用来执行显式转换,返回智能指针内部的原始指针.
此外,auto_ptr和tr1::shared_ptr也重载了指针取值操作符(operator->和operator*),它们允许隐式转换至底部原始指针.示例代码如下:
1 class Investment { 2 public: 3 bool isTaxFree() const; 4 ... 5 }; 6 Investment* createInvestment(); 7 8 std::tr1::shared_ptr<Investment> pi1(createInvestment()); 9 bool taxable1 = !(pi1->isTaxFree()); //经由operator->访问资源 10 ... 11 std::auto_ptr<Investment> pi2(createInvestment()); 12 bool taxable2 = !((*pi2).isTaxFree()); //经由operator*访问资源 13 ...
到目前,假设我们已经实现了自己的资源管理类,但是在使用时,我们还得注意另一个问题.
以独立语句将newed对象存储于智能指针内.如果不这样做,一旦异常被抛出,有可能导致难以察觉的资源泄露.下面以具体示例说明:
1 int priority(); 2 void processWidget(std::tr1::shared_ptr<Widget> pw,int priority); 3 4 processWidget(std::tr1::shared_ptr<Widget>(new Widget),priority()); //调用processWidget函数
编译器产生一个processWidget调用码之前,必须首先核算即将被传递的各个实参.因此在调用processWidget之前,编译器必须创建代码,做以下三件事:
1.调用priority
2.执行"new Widget"
3.调用tr1:shared_ptr构造函数
关键是C++编译器以什么样的次序完成这些事情呢?
C++和其他语言如Java和C#不同,那两种语言总是以特定次序完成函数参数的核算.而C++其具体的执行顺序弹性很大.
如果以下面的顺序进行执行,将会可能产生资源泄露问题.
1.执行"new Widget"
2.调用priority
3.调用tr1::shared_ptr构造函数
这种情况下,如果priority的调用导致异常,"new Widget"返回的指针将会遗失,因为它尚未被置入tr1::shared_ptr内,因而导致资源泄露.
我们必须确保"资源被创建"和"资源被转换为资源管理对象"两个时间点之间不能发生异常干扰.避免上述问题的方法如代码所示:
1 std::tr1::shared_ptr<Widget> pw(new Widget); //在单独语句内以智能指针存储newed所得对象 2 processWidget(pw,priority());
编译器对于"跨语句的各项操作"没有重新排列的自由.因而"资源被创建"和"资源被转换为资源管理对象"两个时间点之间不会发生异常干扰.
参考资料:Effective C++