C++智能指针剖析(上)std::auto_ptr与boost::scoped_ptr
1. 引入
C++语言中的动态内存分配没有自动回收机制,动态开辟的空间需要用户自己来维护,在出函数作用域或者程序正常退出前必须释放掉。 即程序员每次 new 出来的内存都要手动 delete,否则会造成内存泄露, 有时我们已经非常谨慎了 , 然防不胜防:流程太复杂,程序员忘记 delete;异常导致程序过早退出,没有执行delete的情况屡见不鲜。
1 void FunTest() 2 { 3 int *p = new int[10]; 4 FILE* pFile = fopen("1. txt", "w"); 5 if (pFile == NULL) 6 { 7 return; //如果pFile == NULL则p指向的空间得不到释放 8 } 9 // DoSomethint() ; 10 if (p != NULL) 11 { 12 delete[] p; 13 p = NULL; 14 } 15 } 16 void FunTest2() //异常导致程序提前退出 17 { 18 int *p = new int[10]; 19 try 20 { 21 DoSomething(); 22 } 23 catch(. . .) 24 { 25 return; 26 } 27 delete[] p; 28 }
在前面的异常处理一节中已经提到过可以定义一个类来管理资源的分配与初始化,把释放资源的部分交给析构函数来处理,即RAII(Resource Acquisition Is Initialization)直译过来即“资源获取即初始化”,是C++语言的一种管理资源、避免泄漏的惯用法。其实从字面译过来的意思并不全面,因为它只提到了一个方面,还有另一个同样重要的是空间释放与资源回收。RAII:定义一个类来封装资源的分配和释放, 在构造函数中完成资源的分配和初始化, 在析构函数中完成资源的清理, 可以保证资源的正确初始化和释放。C++的这种资源管理机制保证了内存空间的正确使用,在任何情况下,已构造的对象最终会销毁,即它的析构函数最终会被调用。简单的说,RAII 的做法是使用一个对象,在其构造时获取资源,在对象生命期控制对资源的访问使之始终保持有效,最后在对象析构的时候释放资源。
RAII本身不是智能指针,它是一种规范,一种解决问题的思想。智能指针是RAII的一种应用,智能管理资源,可以像指针一样使用,但它不是指针。
2. 智能指针总述
用智能指针便可以有效缓解上面提到的问题,本文主要讲解常见的智能指针的用法。Boost库的智能指针如下:(ps: 新的C++11标准中已经引入了unique_ptr/shared_ptr/weak_ptr)
对于编译器来说,智能指针实际上是一个栈对象,并非指针类型,在栈对象生命周期即将结束时,智能指针通过析构函数释放由它管理的堆内存。所有智能指针都重载了“operator->”和“operator*”操作符,直接返回对象的引用,用以操作对象。访问智能指针原来的方法则使用“.”操作符。(智能指针包含了 reset() 方法,如果不传递参数(或者传递 NULL),则智能指针会释放当前管理的内存。如果传递一个对象,则智能指针会释放当前对象,来管理新传入的对象。)
2.1 被抛弃的std::auto_ptr
std::auto_ptr 属于 STL,当然在 namespace std 中,包含头文件 #include<memory> 便可以使用。std::auto_ptr 能够方便的管理单个堆内存对象。auto_ptr用于指向一个动态分配的对象指针,他的析构函数用于删除所指对象的空间,以此达到对对象生存期的控制。 auto_ptr本质是管理权限的转移。在进行赋值,拷贝构造时,会对控制权进行转移。怎么理解呢?我们用图来解释其中的问题。
上图中的拷贝构造与赋值操作就可以体现auto_ptr 管理资源的本质,为了更清楚的了解auto_ptr ,下面我将模拟实现auto_ptr 的管理机制:
1 template <class T> 2 class AutoPtr 3 { 4 public: 5 AutoPtr(T* ptr = NULL) 6 :_Ptr(ptr) 7 {} 8 AutoPtr(AutoPtr<T>& ap) 9 :_ptr(ap._ptr){ //始终只有一个对象管理一块空间 10 ap._Ptr = NULL; 11 } 12 AutoPtr<T>& operator=(const AutoPtr<T>& ap) { 13 if (this != &ap) { //始终只有一个对象有管理这块空间的权限 14 delete this->_Ptr; 15 this->_ptr = ap._Ptr; 16 ap._Ptr = NULL; 17 } 18 return *this; 19 } 20 ~AutoPtr() { 21 delete _ptr; 22 } 23 T& operator*() { 24 if (_ptr = NULL) { 25 throw ap; //对空指针解引用时抛出异常 26 } 27 return *_ptr; 28 } 29 T* operator->() { 30 if (_ptr = NULL) { 31 throw ap; //使用箭头访问空指针时抛出异常 32 } 33 return _ptr; 34 } 35 bool operator==(const AutoPtr<T>& ap) { 36 return _ptr == ap._Ptr; 37 } 38 bool operator!=(const AutoPtr<T>& ap) { 39 return _ptr != ap._Ptr; 40 } 41 void Reset(T* ptr = NULL) { //删除原有指针_ptr并获得指针ptr的所有权 42 if (_ptr != ptr) { 43 delete _ptr; 44 } 45 _ptr = ptr; 46 }
T* get() const; //返回原始对象的指针
T* release(); //放弃指针的所有权 记住 release() 函数不会释放对象,仅仅归还所有权。
void reset(T* ptr = NULL);//删除原有指针并获得指针的p的所有权 47 private: 48 T* _Ptr; 49 };
上面有两点需要说明:
1,在对“*”进行重载时,如果返回值写成”T ”,而不是“T &”。若出现下面赋值语句:“*P1 = 12”,则编译不会通过。
2,在对“->”进行重载时, 对这条语句:”P1->A = 30” //P1.operator->()->A = 30; 即P1->->A;但是这个可读性太差,编译器进行了优化,P1->A;
总结:std::auto_ptr 可用来管理单个对象的内存,但是,请注意如下几点:
1) 首先auto_ptr智能指针是个封装好的类;
2) 尽量不要使用“operator=”。如果使用了,请不要再使用先前对象;
3) std::auto_ptr 最好不要当成参数传递(读者可以自行写代码确定为什么不能);
4) 采用栈上的指针去管理堆上的内容,所以auto_ptr所管理的对象必须是new出来的,也不能是malloc出来的。(原因:在auto_ptr的实现机制中,采用的是delete 掉一个指针,该delete一方面是调用了指针所指对象的析构函数(这也是为什么采用智能指针,new了一个对象,但是不用delete的原因),另一方面释放了堆空间的内存。)
使用场景总结:
1)不要使用auto_ptr对象保存指向静态分配对象的指针,否则,当auto_ptr对象本身被撤销的时候,它将试图删除指向非动态分配对象的指针,导致未定义的行为。
2)永远不要使用两个 auto_ptrs 对象指向同一对象,导致这个错误的一种明显方式是,使用同一指针来初始化或者 reset 两个不同的 auto_ptr对象。另一种导致这个错误的微妙方式可能是,使用一个 auto_ptr 对象的 get 函数的结果来初始化或者 reset另一个 auto_ptr 对象。(每个智能指针对象析构的时候,都会调用一次delete,导致堆空间内存被释放两次,然而这是不被允许的。)
3)不要使用 auto_ptr 对象保存指向动态分配数组的指针。当auto_ptr 对象被删除的时候,它只释放一个对象—它使用普通delete 操作符,而不用数组的 delete [] 操作符。
4)不要将 auto_ptr 对象存储在容器中。容器要求所保存的类型定义复制和赋值操作符,使它们表现得类似于内置类型的操作符:在复制(或者赋值)之后,两个对象必须具有相同值,auto_ptr 类不满足这个要求。
使用一个 std::auto_ptr 的限制很多,还不能用来管理堆内存数组,如此多的限制就很容易导致问题。所以说它是一个带有缺陷的设计,是一个“弃儿”。由于 std::auto_ptr 引发了诸多问题,一些设计并不是非常符合 C++ 编程思想,所以C++引入了下面 boost 库的智能指针,boost 智能指针可以解决如上问题。
2.2 boost::scoped_ptr
boost库发展的简单介绍:在C++11标准出来之前,C++98标准中都一直只有一个智能指针auto_ptr,我们知道,这是一个失败的设计。它的本质是管理权的转移,这有许多问题。而这时就有一群人开始扩展C++标准库的关于智能指针的部分,他们组成了boost社区,他们负责boost库的开发和维护。其目的是为C++程序员提供免费的、同行审查的、可移植的程序库。boost库可以和C++标准库完美的共同工作,并且为其提供扩展功能。现在的C++11标准库的智能指针很大程度上“借鉴”了boost库。
boost::scoped_ptr 属于 boost 库,定义在 namespace boost 中,包含头文件#include<boost/smart_ptr.hpp> 便可以使用。scoped_ptr 跟 auto_ptr 一样,可以方便的管理单个堆内存对象,特别的是,scoped_ptr 独享所有权,避免了auto_ptr恼人的几个问题。
scoped_ptr是一种简单粗暴的设计,它本质就是防拷贝,避免出现管理权的转移。这是它的最大特点,所以他的拷贝构造函数和赋值运算符重载函数都只是声明而不定义,而且为了防止有的人在类外定义,所以将函数声明为protected。但这也是它最大的问题所在,就是不能赋值拷贝,也就是说功能不全。但是这种设计比较高效、简洁。没有 release() 函数,不会导致先前的内存泄露问题。下面我也将模拟实现scoped_ptr的管理机制:
1 template<class T> 2 class ScopedPtr 3 { 4 public: 5 ScopedPtr(T* ptr = NULL) { 6 :_ptr(ptr) 7 cout << "ScopedPtr()" << end; 8 } 9 ~ScopedPtr(){ 10 delete _ptr; 11 cout << "~ScopedPtr()" << end; 12 } 13 T& operator* (){ 14 return *_ptr; 15 } 16 T* operator->() { 17 return _ptr; 18 } 19 bool operator==(const ScopedPtr<T>& sp) { 20 return _ptr == sp._ptr; 21 } 22 bool operator!=(const ScopedPtr<T>& sp) { 23 return _ptr != sp._ptr; 24 } 25 void Reset(T* ptr = NULL) 26 { 27 if (_ptr != ptr) 28 { 29 delete _ptr; 30 } 31 _ptr = ptr; 32 } 33 protected: 34 ScopedPtr(ScopedPtr<T>& sp); //防拷贝(只声明不定义,为防止别人在类外定义,就将他声明为protected) 35 ScopedPtr<T>& operator=(ScopedPtr<T>& sp); 36 private: 37 T* _ptr; 38 };
scoped_ptr使用特点总结:
1)与auto_ptr类似,采用栈上的指针去管理堆上的内容,从而使得堆上的对象随着栈上对象销毁时自动删除;
2)scoped_ptr有着更严格的使用限制——不能拷贝,这也意味着scoped_ptr不能转换其所有权,所以它管理的对象不能作为函数的返回值,对象生命周期仅仅局限于一定区间(该指针所在的{}区间,而std::auto_ptr可以);
3)由于防拷贝的特性,使其管理的对象不能共享所有权,这与std::auto_ptr类似,这一特点使该指针简单易用,但也造成了功能的薄弱。