一、为什么使用智能指针?智能指针的原理?
C/C++中的堆内存分配和释放的方式主要是: malloc/free 以及 new/delete 等。
使用new 和delete 管理内存存在三个常见问题:
1.忘记delete(释放) 内存,或者异常导致程序过早退出,没有执行 delete。忘记释放动态内存会导致内存泄露问题,长时间这样会导致系统内存越来越小。 (内存泄露问题往往很难查找到,内存耗尽时,才能检测出这种错误)
2.使用已经释放掉的对象。比如:我们使用delete释放掉申请的内存空间,但并未去除指向这片空间的指针,此时指针指向的就是“垃圾”内存。
3.同一块内存释放两次。当有两个指针指向相同的动态内存分配对象时,其中一个进行了delete操作,对象内存就还给了操作系统 ,如果我们要delete第二个指针,那么内存有可能遭到破坏。(浅拷贝问题)
使用智能指针可以很大程度上的避免这些问题。
智能指针就是一个类,类的构造函数中传入一个普通指针,当超出了类的作用域时,类会自动调用析构函数,释放资源。其核心思想是:栈上对象在离开作用范围时会自动析构。
智能指针在C++11版本之后提供,包含在头文件<memory>中,shared_ptr、unique_ptr、weak_ptr,auto_ptr在C++11被弃用。
二、常用的智能指针
1、auto_ptr:自动指针,自动回收。在构造对象时赋予其管理空间的所有权,在拷贝或赋值中转移空间的所有权,拷贝和赋值后直接将_ptr赋为空,禁止其再次访问原来的内存空间。
构造函数:explicit关键字修饰的构造函数不能被隐式调用,只能显示调用
explicit auto_ptr(_Ty *_Ptr = 0) _THROW0() : _Myptr(_Ptr) // 将指针交由auto_ptr托管 { // construct from object pointer }
析构函数:
// 释放了托管的对象所占用的内存空间 ~auto_ptr() { delete _Myptr; }
get方法:
// 返回保存的指针 _Ty *get() const _THROW0() { return (_Myptr); }
release方法:
_Ty *release() _THROW0() { // return wrapped pointer and give up ownership 返回保存的指针,对象中不保留原来的指针,原来的指针直接赋值为0 _Ty *_Tmp = _Myptr; _Myptr = 0; return (_Tmp); }
reset方法:
void reset(_Ty* _Ptr = 0) { // 重置auto_ptr使之拥有另一个对象。先删除已经拥有的对象,然后新建一个并拥有一个新对象 if (_Ptr != _Myptr) delete _Myptr; _Myptr = _Ptr; }
拷贝构造函数:
// 明显可看出会发生托管权的转移 auto_ptr(auto_ptr<_Ty>& _Right) _THROW0() : _Myptr(_Right.release()) { // construct by assuming pointer from _Right auto_ptr }
赋值运算符:
// 很明显也发生了托管权的转移 template<class _Other> auto_ptr<_Ty>& operator=(auto_ptr<_Other>& _Right) _THROW0() { // assign compatible _Right (assume pointer) reset(_Right.release()); return (*this); }
使用:
void fun() { T *pt = new T(); // 将分配的堆内存指针交由auto_ptr托管 std::auto_ptr apt(pt); //显式调用构造函数 // 像正常使用指针一样使用,相当于*pt= 10 *apt = 10; // 相当于 pt->memFunc() apt->memFunc(); // 使用get函数可获取它托管的指针 T *pt2 = apt.get(); // 可调用reset函数更改托管对象,这里删除了之前托管的 pt apt.reset(new T()); // 可调用release函数放弃托管 T *pt3 = apt.release(); // 放弃托管意味着又需要自己手动释放内存了 delete pt3; pt3 = NULL; return; }
注意:
(1) auto_ptr没有使用引用计数,如果多个auto_ptr指向同一个对象,就会造成对象被删除一次以上的错误。因此一个对象只能由一个auto_ptr所拥有,在给其他auto_ptr赋值的时候,会转移这种拥有关系。在赋值、参数传递的时候会转移所有权,不要轻易进行此类操作。
/* 1. 演示转移所有权 */ std::auto_ptr<int> aptr1(new int(3)); // 执行后aptr1不再有效 std::auto_ptr<int> aptr2 = aptr1; // or aptr2(aptr1) // 强行访问会发生不可预料的问题 *aptr1 = 4;
// --------------------------------------------------------
/* 2. 演示参数传递的所有权转移 */ void lose(std::auto_ptr<int> a) { // 空函数,仅仅为了演示参数传递 } std::auto_ptr<int> aptr3(new int(4)); // 所有权转移,aptr3不再有效 lose(aptr3); // 强行访问会发生不可预料的问题 *aptr3 = 10;
(2) auto_ptr的析构函数内部释放资源时调用的是delete而不是delete[],因此不要让auto_ptr托管数组。
(3) auto_ptr不能作为容器对象,因为容器中的元素经常要进行拷贝,赋值等操作,在这过程中auto_ptr会失去所有权。
(4) 判断auto_ptr是否为空不能使用if(aptr == NULL),应该使用if(aptr.get() == NULL)
2、unique_ptr:是指”唯一”地拥有其所指对象的智能指针,同一时刻只能有一个unique_ptr指向给定对象(使用移动语义来实现),与auto_ptr的不同点
(1) 可以通过间接的方式用于容器中
unique_ptr<int> sp(new int(10)); vector<unique_ptr<int> > vec; vec.push_back(std::move(sp)); //通过这种移动语义来实现在容器中使用 vec.push_back(sp); //这样直接使用不行,会报错 cout << *sp << end; //这样也不行,因为sp添加到容器中后,会报错
(2) 无法直接进行复制构造与赋值操作,要使用move函数进行所有权的转移
unique_ptr<int> uq(new int(10)); unique_ptr<int> uq2 = uq; //会报错,auto_ptr中可以 unique_ptr<int> uq3(uq); //同样会报错,auto_ptr中可以
unique_ptr<int> uq4 = std::move(up); //使用move函数 直接显式的所有权转移是可以的
(3) 可以用于函数的返回值
// 函数定义 unique_ptr<int> myFunc() { unique_ptr<int> up(new int(10)); return up; } unique_ptr<int> upRet = myFunc();
(4) 可以直接用if(ptr == NULL)来判断是否空指针
使用:
unique_ptr<int> up(new int(3)); //托管一个对象 // 更改所有权 unique_ptr<int> up2 = std::move(up); //所有权转移,转移后,up变为空指针 int *p = up.release(); //释放所有权 up.reset(); //显式销毁所有权
3、shared_ptr:使用计数机制来表明资源被几个指针共享。与auto_ptr的不同点
(1) 使用一个引用计数shared_count,用来表示当前有多少个智能指针对象共享指针指向的内存块,可以通过成员函数use_count()来查看资源的所有者个数。
(2) 析构函数中对引用计数进行判断,如果 shared_count > 1,则不释放内存只是将引用计数减1,当shared_count == 1的时候释放内存。当调用release()时,当前指针会释放资源所有权,计数减1,当计数等于0时,资源会被释放。
(3) 复制构造与赋值操作符除了提供复制功能之外,还将引用计数加1。
使用:
// 1. 构造方法 // 将指针交由shared_ptr托管 还有一种方式也可以创建shared_ptr对象,且比较常用,通过make_shared函数: shared_ptr<int> shPtr = make_shared<int>(10); shared_ptr<int> shPtr(new int(10)); int num = *shPtr; // 像使用正常指针一样使用它,此时num == 10 // 2. 复制构造函数 shared_ptr<int> shPtr2(shPtr); // 复制构造,此时引用计数会增加 // 两个shared_ptr相等,指向同一个对象,引用计数为2 assert(shPtr == shPtr2 && shPtr.use_count() == 2); // 原先的shPtr还可以继续使用,如果是auto_ptr,是不能使用的,因为有所有权的转移 num = *shPtr; *shPtr = 20; assert(*shPtr2 == 20); // 在改一个shared_ptr的同时,另一个也会更改 // 3. 赋值运算符 shared_ptr<int> shPtr3 = shPtr2; // 赋值操作符 // 4. 停止使用 shPtr.reset(); assert(!shPtr); // shPtr停止使用后会变成空指针
注意:
(1) shared_ptr不能对循环引用的对象的内存进行自动管理,循环引用会导致堆内存无法正确释放,内存泄漏。循环引用在weak_ptr中介绍。
(2) 不要构造一个临时的shared_ptr作为函数的参数,存在内存泄漏的风险
void f(shared_ptr<int>, int); int g(); // 正确的使用方式 void OK() { shared_ptr<int> p(new int(2)); f(p, g()); } // 错误的使用方式 void Bad() { // 如果执行顺序是先new int(2), 然后g(), 最后将 new int(2)的指针给shared_ptr的构造函数的话,当g()中抛出异常的时候,第一个new int(2)就造成了内存泄漏了 f(shared_ptr<int>(new int(2)), g()); }
4、weak_ptr:相对于shared_ptr这种强引用类型的智能指针, weak_ptr是一种弱引用型的指针。用来解决shared_ptr相互引用时的死锁问题,如果说两个shared_ptr相互引用,那么这两个指针的引用计数永远不可能下降为0,资源永远不会释放。可以看成是shared_ptr的助手而不是真正的智能指针,因为它不会托管资源,它的构造也不会引起引用计数的增加,且没有重载 operator * 和 operator ->,不具有普通指针的行为。和shared_ptr之间可以相互转化,shared_ptr可以直接赋值给它,它可以通过调用lock函数来获得shared_ptr。
循环引用:
class B; class A { public: A() { cout << "Class A Constructor is called." << endl; } ~A() { cout << "Class A Deconstructor is called." << endl; } //tr1::shared_ptr<B> m_shB; tr1::weak_ptr<B> m_shB; }; class B { public: B() { cout << "Class B Constructor is called." << endl; } ~B() { cout << "Class B Deconstructor is called." << endl; } tr1::shared_ptr<A> m_shA; }; int _tmain(int argc, _TCHAR* argv[]) { { // 测试重复引用 tr1::shared_ptr<A> shA(new A()); //shA引用计数:1 tr1::shared_ptr<B> shB(new B()); //shB引用计数:1 if (shA && shB) { shA->m_shB = shB; //因为m_shB是weak_ptr对象,不会引起计数增加,shB引用计数仍为1。若m_shB是shared_ptr对象,则shB引用计数变为2. shB->m_shA = shA; //shA引用计数变为2 } cout << "A的引用计数:" << shA.use_count() << " B的引用计数:" << shB.use_count() << endl; cout << "要离开shA和shB的作用域了,正常情况下在这之后会执行shA和shB的析构函数的" << endl; // 这里是要执行析构函数的 // 首先,会执行shB这个B对象的析构函数,要析构B的话,得先去判断下托管B的shared_ptr的引用计数,(若这里是2,则不能析构B,B的成员对象A自然也不能析构,从而死锁) // 这里是1,所以去析构B,B析构后紧接着去析构其成员对象A,此时A的引用计数为2,所以会使A的引用计数减为1 // 然后,会执行shA这个A对象的析构函数,要析构A的话,也得先去判断下托管的A的shared_ptr的引用计数,这里是1,它可以析构 } cout << "已经离开shA和shB的作用域了,请观察shA和shB的析构函数有没有被执行" << endl; }
参考:https://jocent.me/2017/05/31/cpp_smart_pointer.html