条款13:以对象管理资源
所谓资源就是,一旦使用了它,将来必须还给系统。C++最常使用的资源就是动态分配内存(如果分配了内存却不释放,会导致内存泄露),但内存只是必须要管理的众多资源之一。其他常见的资源还包括文件描述器(file descriptors)、互斥锁(mutex locks)、图形界面中的字型和笔刷、数据库连接、以及网络sockets。不论哪一种资源,重要的是,当不再使用它时,必须将它还给系统。
假设我们使用一个用来模拟投资行为(例如股票、债券等)的程序库,其中各式各样的投资类型继承自一个root class Investment:
class Investment { ... //"投资类型" 继承体系中的root class };
进一步假设,这个程序库系通过一个工厂函数供应我们某特定的Investment对象:
Investment* createInvestment(); //返回指针,指向Investment继承体系内的动态分配对象。调用者有责任删除它。
//这里为了简化,不写参数
为确保createInvestment返回的资源总是被释放,把资源放进对象内,我们便可依赖C++的“析构函数自动调用机制”确保资源被释放。
许多资源被动态分配在heap堆内而后被用于单一区块或函数内。它们应该在控制流离开那个区块或函数时被释放。标准程序库提供的auto_ptr正是针对这种形式而设计的特制产品。auto_ptr是个“类指针(pointer-like)对象”,也就是所谓的“智能指针”,其析构函数自动对其所指对象调用delete。下面示范如何使用auto_ptr以避免f函数潜在的资源泄露可能性:
void f() { std::auto_ptr<investment> pInv(createInvestment()); //调用工厂函
... //一如既往地使用pInv
//经由auto_ptr的析构函数自动删除pInv }
这个简单的例子示范了“以对象管理资源”的两个关键想法:
1、获得资源或立刻放进管理对象(managing object)内。以上代码中的createInvestment返回的资源被当做其管理者auto_ptr的初值。实际上“以对象管理资源”的观念常被成为“资源取得时机便是初始化时机”(Resource Acquisition Is Initialization;RAII),因为我们几乎总是在获得一笔资源后于同一语句内以它初始化某个管理对象。
2、管理对象(managing object)运用析构函数确保资源被释放。不论控制流如何离开区块,一旦对象被销毁(例如当对象离开作用域)其析构函数自然会被自动调用于是资源被释放。
注意:由于auto_ptr被销毁时会自动删除它所指之物,所以一定要注意别让多个auto_ptr同时指向同一个对象。否则若对象被删除一次以上,会出现“未定义行为”的错误,为了预防这个问题,auto_ptr有个一特殊性质:若通过copy构造函数或copy assignment操作符复制它们,它们会变成null,而复制所得的指针将取得资源的唯一拥有权!
std::auto_ptr<Investment> pInv1(CreateInvestment()); //pInv1指向CreateInvestment返回物
std::auto_ptr<investment> pInv2(pInv1); //现在pInv2指向对象,pInv1被设为null
pInv1 = pInv2; //现在pInv1指向对象,pInv2被设为null
auto_ptr的替代方案是“引用计数型智慧指针”(reference-counting smart pointer; RCSP)。所谓RCSP也是个智能指针,持续追踪共有多少对象指向某笔资源,并在无人指向它时自动删除该资源。RCSP提供的行为类型垃圾回收,不同的是RCSP无法打破环状引用(cycles of reference,例如两个其实已经没有被使用的对象彼此互指,因而好像还处在“被使用”状态)。
TR1的tr1::shared_ptr(见条款54)就是个RCSP,所以可以这样写f:
void Func() { ... std::tr1::shared_ptr<Investment> pInv1(createInvestment()); //pInv1指向crateInvestment返回物 std::tr1::shared_ptr<Investment> pInv2(pInv1); //pInv1和pInv2指向同一个对象 pInv1 = pInv2; //同上,无任何改变 ... } //函数结束后,pInv1和pInv2被销毁,它们所指的对象也被自动销毁
因为auto_ptr并不是完美无缺的,它的确很方便,但也有缺陷,在使用时要注意避免。首先,不要将auto_ptr对象作为STL容器的元素。C++标准明确禁止这样做,否则可能会碰到不可预见的结果。
auto_ptr和tr1::shared_ptr两者都在其析构函数内做delete而不是delete[]动作。故不能讲动态分配的数组上使用auto_ptr或tr1::shared_ptr。例如:
std::auto_ptr<std::string> aps(new std::string[10]); //错误!会用上错误的delete形式
std::tr1::shared_ptr<int> spi(new int[1024]); //错误!会用上错误的delete形式
请牢记:
1、为防止内存泄露,请使用RAII对象,它们在构造函数中获得资源并在析构函数中释放资源。
2、两个常被使用RAII class分别是trl1::shared_ptr和auto_ptr。trl1::shared_ptr通常是较佳选择,因为其copy行为比较直观。若选择auto_ptr,复制动作会使被复制物指向null。trl1::shared_ptr在头文件<memory>中
然后收集了关于auto_ptr的几种注意事项:
1、auto_ptr不能共享所有权。
2、auto_ptr不能指向数组
3、auto_ptr不能作为容器的成员。
4、不能通过赋值操作来初始化auto_ptr
std::auto_ptr<int> p(new int(42)); //OK
std::auto_ptr<int> p = new int(42); //ERROR
这是因为auto_ptr 的构造函数被定义为了explicit
5、不要把auto_ptr放入容器
然后笔者从而推荐的是boost的shared_ptr,然后看完shared_ptr关于智能指针的介绍与例子。
5种针对auto_ptr不足的指针如下:需要详细了解可以去查看相当文档,与测试新代码。
scoped_ptr | <boost/scoped_ptr.hpp> | 简单的单一对象的唯一所有权。不可拷贝。 |
scoped_array | <boost/scoped_array.hpp> | 简单的数组的唯一所有权。不可拷贝。 |
shared_ptr | <boost/shared_ptr.hpp> | 在多个指针间共享的对象所有权。 |
shared_array | <boost/shared_array.hpp> | 在多个指针间共享的数组所有权。 |
weak_ptr | <boost/weak_ptr.hpp> | 一个属于 shared_ptr 的对象的无所有权的观察者。 |
intrusive_ptr | <boost/intrusive_ptr.hpp> | 带有一个侵入式引用计数的对象的共享所有权。 |
1. shared_ptr是Boost库所提供的一个智能指针的实现,shared_ptr就是为了解决auto_ptr在对象所有权上的局限性(auto_ptr是独占的),在使用引用计数的机制上提供了可以共享所有权的智能指针。
2. shared_ptr比auto_ptr更安全
3. shared_ptr是可以拷贝和赋值的,拷贝行为也是等价的,并且可以被比较,这意味这它可被放入标准库的一般容器(vector,list)和关联容器中(map)。