C++中的智能指针
1. 从auto_ptr说起
先来一段简单的代码,看看auto_ptr
的使用:
/************************************************************************ > File Name: auto_ptrDemo.cpp > Author:Jelly > Mail:vipygd#126.com(#=>@) > Created Time: 2014年10月15日 星期三 12时00分33秒 ************************************************************************/ #include <iostream> #include <memory> using namespace std; class A { public: A() { cout<<"Construct A Object."<<endl; } ~A() { cout<<"Destroy A Object."<<endl; } }; int main() { auto_ptr<int> pInt(new int(10)); cout<<*pInt<<endl; auto_ptr<A> pAObj(new A); return 0; }
哦,被你发现了;auto_ptr
的初衷是用来实现智能指针的,实现内存的自动回收。比如,代码中我new
了一个A对象,但是却没有对应的delete A
对象。嗯,这得解决多少麻烦事啊,真的是好东西,而且还这么好用。好用?如果好用,我就不用写这篇博文来进行总结了。说道智能指针,就不得不对它的实现原理简单说说。
2. 智能指针
智能指针,无非就是进行垃圾回收,主要可以分为以下两大类:
- 基于引用计数的垃圾回收器。简单的说,引用计数主要是使用系统记录对象被引用的次数。当对象被引用的次数变为0时,该对象即可被视为“垃圾”,从而可以被回收。使用引用计数做垃圾回收的算法的一个优点是实现很简单,与其它垃圾回收算法相比,该方法不会造成程序暂停,因为计数的增减与对象的使用时紧密结合的。 具体的引用实例可以参见:
- 基于跟踪处理的垃圾回收机制。相比于引用计数,跟踪处理的垃圾回收机制被更为广泛地应用。其基本方法是产生跟踪对象的关系图,然后进行垃圾回收。使用跟踪方式的垃圾回收算法主要有以下几种:
- 标记-清除(Mark-Sweep)
这个算法可以分为两个过程。首先该算法将程序中正在使用的对象视为“根对象”,从根对象开始查找它们所引用的堆空间,并在这些堆空间上做标记。当标记结束后,所有被标记的对象就是可达对象或活对象,而没有被标记的对象就被认为是垃圾,在第二步的清扫阶段会被回收掉;
这种方法的特点是活的对象不会被移动,但是其存在会出现大量的内存碎片问题。 - 标记-整理(Mark-Compact)
这个算法标记的方法和标记-清除方法一样,但是标记完之后,不再遍历所有对象清扫垃圾了,而是将活跃的对象向“左”靠齐,这就解决了内存碎片的问题。
标记-整理算法有个特点就是移动活的对象,因此相应的,程序中所有对堆内存的引用都要更新。 - 标记-拷贝(Mark-Copy)
这种算法的一大特点就是将堆空间分为两部分:From,To。开始的时候我们只在From里分配,当From分配满的时候出发垃圾收集,这个时候会找出From空间里所有的存活对象,然后将这些存活的对象拷贝到To空间里。这样From空间里剩下的就都全是垃圾,而且对象拷贝到To里,在To里是紧凑排列的。这个事儿做完了之后From和To的角色就转变了一下。原来的From变成了To,原来的To变成了现在的From。现在又可以在这个完全是空的From里分配了。这个算法实现起来也很简单,高效(Sun JVM的新生代的垃圾回收就使用了这种算法)。不过这个算法有一个问题,堆的利用率只有一半了,这对那些内存占用率比较低的对象还算好,如果随着应用的内存占用率的增高,问题就出现了,第一个要拷贝的对象太多了,还有可能无法回收内存了。程序失败了。
- 标记-清除(Mark-Sweep)
3. 为什么auto_ptr难用?
总结了一点理论性的东西,我是最讨厌理论的,但是没有办法,你越讨厌的东西,你还越要去看,无奈。话题再收回来,上面也说了,auto_ptr
不好用,为什么说auto_ptr
不好用呢?在继续阅读下面的内容之前,我建议阅读一下这篇文章:《C++中的RAII机制》。 是的,auto_ptr
并没有使用上面介绍的几种垃圾回收技术中的任何一种技术,而是使用的一种叫做RAII
的机制实现的,所以,auto_ptr
本质上一点都不智能。来看下面这段代码:
#include <iostream> #include <memory.h> using namespace std; class A { public: A() { cout << "Construct A Object." << endl; } ~A() { cout << "Destroy A Object." << endl; } void SetA(int value) { m_a = value; } int GetA() { return m_a; } private: int m_a; }; void TestFunc(auto_ptr<A> Obj) { Obj->SetA(20); cout << Obj->GetA() << endl; } int main() { auto_ptr<A> pAObj(new A()); //auto_ptr<A> pAObj1 = new A(); **This is wrong expression. pAObj->SetA(10); cout << pAObj->GetA() << endl; TestFunc(pAObj); //cout << pAObj->GetA() << endl; ** This is wrong. return 0; }
看看,发生了什么。调用完TestFunc(pAobj)
之后,我再调用pAObj->GetA()
居然出错了。这就是auto_ptr
的奇葩之处。为什么?在《C++中的RAII机制》中的使用RAII的陷阱
一节有仔细的分析。
4. auto_ptr的庐山真面目
总是在说,总觉的缺点什么。看看auto_ptr
的源代码吧(代码来源:SGI STL)。
template <class _Tp> class auto_ptr { private: _Tp* _M_ptr; //实际wrap的指针 public: typedef _Tp element_type; // 显式构造函数,防止auto_ptr<A> pAObj1 = new A();隐式构造 explicit auto_ptr(_Tp* __p = 0) __STL_NOTHROW : _M_ptr(__p) {} // 复制构造函数,知道为什么参数是&吗? auto_ptr(auto_ptr& __a) __STL_NOTHROW : _M_ptr(__a.release()) {} template <class _Tp1> auto_ptr(auto_ptr<_Tp1>& __a) __STL_NOTHROW : _M_ptr(__a.release()) {} // 赋值构造函数 auto_ptr& operator=(auto_ptr& __a) __STL_NOTHROW { if (&__a != this) { delete _M_ptr; _M_ptr = __a.release(); } return *this; } template <class _Tp1> auto_ptr& operator=(auto_ptr<_Tp1>& __a) __STL_NOTHROW { if (__a.get() != this->get()) { delete _M_ptr; _M_ptr = __a.release(); } return *this; } ~auto_ptr() { delete _M_ptr; } // 智能指针一般都要重载"*"和"->"操作符 _Tp& operator*() const __STL_NOTHROW { return *_M_ptr; } _Tp* operator->() const __STL_NOTHROW { return _M_ptr; } _Tp* get() const __STL_NOTHROW { return _M_ptr; } _Tp* release() __STL_NOTHROW { _Tp* __tmp = _M_ptr; _M_ptr = 0; return __tmp; } void reset(_Tp* __p = 0) __STL_NOTHROW { if (__p != _M_ptr) { delete _M_ptr; _M_ptr = __p; } } };
对于上面的代码,你有什么要问的么?阅读代码就会发现,auto_ptr
不能共享内存,在同一时间,只有一个auto_ptr
指向一个指定的内存。所以说,以下这种代码就会ptr2
失效了。
auto_ptr<int> ptr(p);
auto_ptr<int> ptr2(p);
ptr = ptr2;
简直无法想象,一个赋值操作,导致右值失效了,怎么可以,这种问题,在实际开发中,我们不得不去注意。
5. 我们要注意什么?
是的,auto_ptr
有不少坑,不可能列全了,但话又说回来了,以后基本上也会告别auto_ptr
了,让我们再和auto_ptr
愉快的玩最后一段时光吧。 当你、我,还有他在使用auto_ptr
时,需要注意以下几点:
auto_ptr
不能共享所有权;auto_ptr
不能指向数组(否则会造成内存泄漏问题);对于这点,我认为如果数组中存放的是POD类型时,或者含有trivial destructor时,是可以指向数组的;auto_ptr
不能作为容器的成员;C++标准已经明确禁止这么做了,否则可能会遇到不可预见的问题(STL容器在分配内存的时候,必须要能够拷贝构造容器的元素。而且拷贝构造的时候,不能修改原来元素的值。而auto_ptr在拷贝构造的时候,一定会修改元素的值。所以STL元素不能使用auto_ptr。)。
6. C++11中的智能指针
但是随着C++11的到来,auto_ptr
已经不再了,即将成为历史;好的东西总是会受到大家的欢迎的,随着大家都在使用“准”标准库boost中的shared_ptr
;C++标准委员会终于觉的是时候将shared_ptr
加入到C++11中去了。欢呼声一片,至少我是这么觉的了;至少shared_ptr
让我用起来,还是不错的。接下来,就总结一下C++11中的这些智能指针吧。
先来一段简单的代码,看看C++11中到底有哪些智能指针。
/************************************************************************* > File Name: SmartPointDemo.cpp > Author: Jelly > Mail: vipygd#126.com(#->@) > Created Time: 2014年10月16日 星期四 15时25分43秒 ************************************************************************/ #include <iostream> #include <memory> using namespace std; int main() { unique_ptr<int> up1(new int(10)); // 不能复制的unique_ptr // unique_ptr<int> up2 = up1; // 这样是错的 cout<<*up1<<endl; unique_ptr<int> up3 = move(up1); // 现在up3是数据唯一的unique_ptr智能指针 cout<<*up3<<endl; // cout<<*up1<<endl; // 运行时错误 up3.reset(); // 显式释放内存 up1.reset(); // 即使up1没有拥有任何内存,但是这样调用也没有问题 // cout<<*up3<<endl; // 已经释放掉up3了,这样会运行时错误 shared_ptr<int> sp1(new int(20)); shared_ptr<int> sp2 = sp1; // 这是完全可以的,增加引用计数 cout<<*sp1<<endl; cout<<*sp2<<endl; sp1.reset(); // 减少引用计数 cout<<*sp2<<endl; return 0; }
C++11中主要提供了unique_ptr
、shared_ptr
和weak_ptr
这三个智能指针来自动回收堆分配的对象。看看上面的代码,感觉用起来也还挺轻松的,也还不错,至少是比auto_ptr
好点。
7. unique_ptr
C++11中的unique_ptr
是auto_ptr
的替代品,它与auto_ptr
一样拥有唯一拥有权的特性,与auto_ptr
不一样的是,unique_ptr
是没有复制构造函数的,这就防止了一些“悄悄地”丢失所有权的问题发生,如果需要将所有权进行转移,可以这样操作:
unique_ptr<int> up3 = move(up1); // 现在up3是数据唯一的unique_ptr智能指针
// 或者
unique_ptr<int> up4(move(up1));
只有在使用者显式的调用std::move
之后,才会发生所有权的转移,这样就让使用者知道自己在干什么。再来一段代码,看看将unique_ptr
作为函数参数和返回值的使用情况:
/************************************************************************* > File Name: unique_ptrDemo.cpp > Author: Jelly > Mail: vipygd#126.com(#->@) > Created Time: 2014年10月16日 星期四 17时10分49秒 ************************************************************************/ #include <iostream> #include <memory> using namespace std; unique_ptr<int> Func(unique_ptr<int> a) { cout<<*a<<endl; return a; } int main() { unique_ptr<int> up1(new int(10)); up1 = Func(move(up1)); cout<<*up1<<endl; return 0; }
由于在unique_ptr
中是没有拷贝构造函数的,所以在直接传参时,进行值传递时,建立临时变量时,就会出错了,所以需要显式的调用move
,转移所有权;而函数的返回值已经进行了move
操作,而不用显式的进行调用。
8. shared_ptr
在最开始的那段代码中,也简单的使用了一下shared_ptr
。shared_ptr
名如其名,它允许多个该智能指针共享地“拥有”同一堆分配对象的内存;由于它的资源是可以共用的,所以也就可以透过operator=
等方法,来分享shared_ptr
所使用的资源。由于shared_ptr
内部实现上使用的是引用计数这种方法,所以一旦一个shared_ptr
指针放弃了“所有权”,其它的shared_ptr
对对象的引用并不会发生变化;只有在引用计数归零的时候,shared_ptr
才会真正的释放所占有的堆内存空间的。对于引用计数的问题,我这里就不再多总结了,可以参考以下文章:
– 智能指针-引用计数实现
– COM中的引用计数1
– COM中的引用计数2
我这里注重的总结shared_ptr
的使用,并不会对shared_ptr
进行源码级别的分析。再来一段简单的代码,看看shared_ptr
的一些应用。
#include <iostream> #include <memory> using namespace std; void Func1(shared_ptr<int> a) { cout<<"Enter Func1"<<endl; cout<<"Ref count: "<<a.use_count()<<endl; cout<<"Leave Func1"<<endl; } shared_ptr<int> Func2(shared_ptr<int> a) { cout<<"Enter Func2"<<endl; cout<<"Ref count: "<<a.use_count()<<endl; cout<<"Leave Func2"<<endl; return a; } int main() { shared_ptr<int> aObj1(new int(10)); cout<<"Ref count: "<<aObj1.use_count()<<endl; { shared_ptr<int> aObj2 = aObj1; cout<<"Ref count: "<<aObj2.use_count()<<endl; } Func1(aObj1); Func2(aObj1); shared_ptr<int> aObj3 = Func2(aObj1); cout<<"Ref count:"<<aObj3.use_count()<<endl; return 0; }
自己单独想想程序的输出。输出如下:
Ref count: 1
Ref count: 2
Enter Func1
Ref count: 2
Leave Func1
Enter Func2
Ref count: 2
Leave Func2
Enter Func2
Ref count: 2
Leave Func2
Ref count:2
9. shared_ptr指向数组
在默认情况下,shared_ptr
将调用delete
进行内存的释放;当分配内存时使用new[]
时,我们需要对应的调用delete[]
来释放内存;为了能正确的使用shared_ptr
指向一个数组,我们就需要定制一个删除函数,例如:
#include <iostream> #include <memory> using namespace std; class A { public: A() { cout<<"constructor"<<endl; } ~A() { cout<<"destructor"<<endl; } }; int main() { shared_ptr<A> arrayObj(new A[5], [](A *p){delete[] p;}); return 0; }
上面的代码看不懂的,请参考这篇《C++中的Lambda表达式》文章。如果确实需要共享地托管一个对象,使用unique_ptr
也许会更简单一些,比如:
#include <iostream> #include <memory> using namespace std; class A { public: A() { cout<<"constructor"<<endl; } ~A() { cout<<"destructor"<<endl; } }; int main() { unique_ptr<A[]> arrayObj(new A[5]); return 0; }
10. 线程安全
关于多线程中使用shared_ptr
,有如下几点描述:
1. 同一个shared_ptr
被多个线程读,是线程安全的;
2. 同一个shared_ptr
被多个线程写,不是 线程安全的;
3. 共享引用计数的不同的shared_ptr
被多个线程写,是线程安全的。 对于第一点,没有什么说的;对于第二点,同一个shared_ptr
在不同的线程中进行写操作不是线程安全的,那基于第三点,我们一般会有以下方案来实现线程安全:
对于线程中传入的外部
shared_ptr
对象,在线程内部进行一次新的构造,例如: sharedptr AObjTmp = outerSharedptrObj;
11. 环形引用
对于使用引用计数实现的智能指针,总是避免不了这个问题的。如果出现类似下面的代码,那就出现了环形引用的问题了。
class Parent { public: shared_ptr<Child> child; }; class Child { public: shared_ptr<Parent> parent; }; shared_ptr<Parent> pA(new Parent); shared_ptr<Child> pB(new Child); pA->child = pB; pB->parent = pA;
要解决环形引用的问题,没有特别好的办法,一般都是在可能出现环形引用的地方使用weak_ptr
来代替shared_ptr
。说到了weak_ptr
,那下面就接着总结weak_ptr
吧。
12. weak_ptr
weak_ptr
是最麻烦的,也比较拗口的;它可以指向shared_ptr
指针指向的对象内存,却并不拥有该内存。但是,使用weak_ptr
成员lock
,则可返回其指向内存的一个shared_ptr
对象,且在所指对象内存已经无效时,返回指针空值(nullptr)。由于weak_ptr
是指向shared_ptr
所指向的内存的,所以,weak_ptr
并不能独立存在。例如以下代码:
#include <iostream> #include <memory> using namespace std; void Check(weak_ptr<int> &wp) { shared_ptr<int> sp = wp.lock(); // 重新获得shared_ptr对象 if (sp != nullptr) { cout<<"The value is "<<*sp<<endl; } else { cout<<"Pointer is invalid."<<endl; } } int main() { shared_ptr<int> sp1(new int(10)); shared_ptr<int> sp2 = sp1; weak_ptr<int> wp = sp1; // 指向sp1所指向的内存 cout<<*sp1<<endl; cout<<*sp2<<endl; Check(wp); sp1.reset(); cout<<*sp2<<endl; Check(wp); sp2.reset(); Check(wp); return 0; }
所以,我们在使用weak_ptr
时也要当心,时刻需要判断对应的shared_ptr
是否还有效。对于上面的环形引用的问题,由于weak_ptr
并不会增加shared_ptr
的引用计数,所以我们就可以使用weak_ptr
来解决这个问题。
class Parent { public: weak_ptr<Child> child; }; class Child { public: weak_ptr<Parent> parent; }; shared_ptr<Parent> pA(new Parent); shared_ptr<Child> pB(new Child); pA->child = pB; pB->parent = pA;