C++智能指针
c++
本文介绍c++里面的四个智能指针: auto_ptr, shared_ptr, weak_ptr, unique_ptr 其中后三个是c++11支持,并且第一个已经被c++11弃用。
为什么要使用智能指针:我们知道c++的内存管理是让很多人头疼的事,当我们写一个new语句时,一般就会立即把delete语句直接也写了,但是我们不能避免程序还未执行到delete时就跳转了或者在函数中没有执行到最后的delete语句就返回了,如果我们不在每一个可能跳转或者返回的语句前释放资源,就会造成内存泄露。使用智能指针可以很大程度上的避免这个问题,因为智能指针就是一个类,当超出了类的作用域是,类会自动调用析构函数,析构函数会自动释放资源。下面我们逐个介绍。
auto_ptr 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 #include <iostream> #include <memory> //auto_ptr的头文件 using namespace std ;class Test { public : Test(string s) { str = s; cout << "Test creat\n" ; } ~Test() { cout << "Test delete:" << str << endl ; } string &getStr () { return str; } void setStr (string s) { str = s; } void print () { cout << str << endl ; }private : string str; };int main () { auto_ptr <Test> ptest (new Test("123" )) ; ptest->setStr("hello " ); ptest->print (); ptest.get ()->print (); ptest->getStr() += "world !" ; (*ptest).print (); ptest.reset(new Test("123" )); ptest->print (); system("pause" ); return 0 ; }
输出:
1 2 3 4 5 6 7 Test creat hello hello hello world ! Test creat Test delete :hello world !123
如上面的代码:智能指针可以像类的原始指针一样访问类的public成员,成员函数get()返回一个原始的指针,成员函数reset()重新绑定指向的对象,而原来的对象则会被释放。注意我们访问auto_ptr的成员函数时用的是“.”,访问指向对象的成员时用的是“->”。我们也可用声明一个空智能指针
auto_ptr<Test>ptest();
当我们对智能指针进行赋值时,如ptest2 = ptest
,ptest2会接管ptest原来的内存管理权,ptest会变为空指针,如果ptest2原来不为空,则它会释放原来的资源,基于这个原因,应该避免把auto_ptr放到容器中,因为算法对容器操作时,很难避免STL内部对容器实现了赋值传递操作,这样会使容器中很多元素被置为NULL。判断一个智能指针是否为空不能使用if(ptest == NULL),应该使用if(ptest.get() == NULL),如下代码:
1 2 3 4 5 6 7 8 9 10 11 int main () { auto_ptr <Test> ptest (new Test("123" )) ; auto_ptr <Test> ptest2 (new Test("456" )) ; ptest2 = ptest; ptest2->print (); if (ptest.get () == NULL ) cout << "ptest = NULL\n" ; system("pause" ); return 0 ; }
输出:
1 2 3 4 5 Test creat Test creat Test delete :456 123 ptest = NULL
还有一个值得我们注意的成员函数是release,这个函数只是把智能指针赋值为空,但是它原来指向的内存并没有被释放,相当于它只是释放了对资源的所有权,从下面的代码执行结果可以看出,析构函数没有被调用。
1 2 3 4 5 6 int main () { auto_ptr <Test> ptest (new Test("123" )) ; ptest.release (); return 0 ; }
输出:
那么当我们想要在中途释放资源,而不是等到智能指针被析构时才释放,我们可以使用ptest.reset() 语句。
1 2 3 4 5 6 7 int main () { auto_ptr <Test> ptest (new Test("123" )) ; ptest.reset(); system("pause" ); return 0 ; }
输出:
1 2 Test creat Test delete :123
auto_ptr的弊端
“=”运算符的重载
1 2 my_auto2 = my_auto1 my_auto1->PringSomething();
autp_ptr对赋值运算符重载的实现是reset(Myptr.release())
,即reset和release函数的组合。release会释放所有权,reset则是用于接受所有权。my_auto1被release之后已经被置0(内部实现),所以调用函数当然会出错。
临时对象
1 2 3 4 5 6 7 8 9 10 11 void Fun (auto_ptr <Test> ap) { *ap; } void TestAutoPtr3 () { auto_ptr <Test> my_auto (new Test(1 )) ; Fun(my_auto); my_auto->PrintSomething(); }
这个错误非常隐蔽,基本很难发现。可以看到在调用Fun函数之后,my_auto竟然又被置空了,所以导致调用PrintSomething方法崩溃。
说到底,还是因为我们在Fun函数的参数列表中使用的是值传递 。值传递的情况下在调用Fun函数时,由于不会对本身进行修改,所以会产生一个临时对象 来接受参数。是不是熟悉的问题?又出现了赋值运算符 ,因为auto_ptr对赋值运算符重载的关系原指针就被置空了。只不过这次Fun函数结束后临时对象也被抛弃,my_auto也置空,保存的那块内存就彻底丢失了,也造成了内存泄漏,这可是真正上的危机。
解决的方法也很简单,Fun函数参数改为引用传递就可以了。
直接调用release就会导致内存泄漏
1 2 3 4 5 6 7 8 9 void TestAutoPtr4 () { auto_ptr <Test> my_auto(new Test(1 )); if (my_auto.get ()) { my_auto.reset(); } }
直接调用release就会导致内存泄漏。在不了解release和reset的实现时,经常会出现不知道使用哪个而导致内存泄漏的问题。在此我们就对这两个函数进行分析和区别。
1 2 3 4 5 6 7 8 9 10 11 12 13 _Ty *release () _THROW0 () { _Ty *_Tmp = _Myptr; _Myptr = 0 ; return (_Tmp); }void reset (_Ty *_Ptr = 0 ) { if (_Ptr != _Myptr) delete _Myptr; _Myptr = _Ptr; }
以上是memory头文件中的release和reset源码。
先看release函数。定义一个指针来接受myptr,然后将myptr置空,最后return 临时指针。如果直接调用该方法而不去使用别的指针进行接受的话,就会引起内存泄漏。这个函数意在将调用该函数的智能指针的所有权转移 ,如 ptr = my_auto2.release();就是将my_auto2的所有权转给ptr。(这个代码只是伪代码,有助于理解release函数,实际上需要考虑赋值运算符重载的问题)。
当然你也可以直接my_auto.release();而左边不用任何东西去接收,只不过这样就会导致内存泄漏。
再来看reset函数。它有一个参数,默认下是空。首先进行myptr和参数的比较。如果此时没有参数(即为默认的0)且此时myptr非空,就是真正的reset,将myptr delete掉,然后将myptr置空,完成了释放内存的操作。另一种情况就是有参数的情况,只要myptr和参数不相等,就直接delete myptr,然后把参数再赋给myptr。删除旧对象,保存了一个新的对象。
最后看一下赋值运算符重载的实现,是release和reset的组合。
1 2 3 4 5 _Myt& operator =(_Myt& _Right) _THROW0() { reset(_Right.release ()); return (*this ); }
a = b,其中b即为参数Right。首先将b release,b被置空,绑定的对象返回作为reset的参数,this指针调用reset即将this指针中的内容delete,然后接收release返回的对象,就完成了赋值运算符的模拟。
unique_ptr unique_ptr 是用于取代c++98 auto_ptr 的产物,在c++98的时候还没有移动语义 (move semantics) 的支持,因此对于auto_ptr 的控制权转移的实现没有核心元素的支持,但还是实现了auto_ptr 的移动语义,这样带来的一些问题是拷贝构造函数和复制操作重载函数不够完美,具体体现就是把 auto_ptr 作为函数参数传进去的时候控制权转移到函数参数,当函数返回的时候并没有一个控制权移交的过程,所以过了函数调用则原先的 auto_ptr 已经失效了。
在c++11当中有了移动语义,使用 move() 把unique_ptr 传入函数,这样你就知道原先的 unique_ptr 已经失效了。再一个 auto_ptr 不支持传入deleter,所以只能支持单对象(delete object),而 unique_ptr 对数组类型有偏特化重载,并且还做了相应的优化,比如用 [] 访问相应元素等。
unique_ptr 是一个独享所有权的智能指针,它提供了严格意义上的所有权,包括:
拥有它指向的对象
无法进行复制构造,无法进行复制赋值操作。即无法使两个unique_ptr 指向同一个对象。但是可以进行移动构造和移动赋值操作
保存指向某个对象的指针,当它本身被删除释放的时候,会使用给定的删除器释放它指向的对象
unique_ptr 可以实现如下功能:
为动态申请的内存提供异常安全
讲动态申请的内存所有权传递给某函数
从某个函数返回动态申请内存的所有权
在容器中保存指针
auto_ptr 应该具有的功能
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 unique_ptr <Test> fun () { return unique_ptr <Test>(new Test("789" )); }int main () { unique_ptr <Test> ptest (new Test("123" )) ; unique_ptr <Test> ptest2 (new Test("456" )) ; unique_ptr <Test> ptest3 (new Test("abc" )) ; unique_ptr <Test> ptest4 (new Test("def" )) ; ptest->print (); ptest2 = std ::move (ptest); if (ptest == NULL )cout <<"ptest = NULL\n" ; Test* p = ptest2.release (); p->print (); ptest.reset(p); ptest3.reset(p); ptest->print (); ptest2 = fun(); ptest2->print (); system("pause" ); return 0 ; }
输出:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 Test creat Test creat Test creat Test creat123 Test delete :456 ptest = NULL 123 Test delete :abc123 Test creat789 请按任意键继续. . . Test delete :def Test delete :123 Test delete :789 Test delete :
unique_ptr 和 auto_ptr用法很相似,不过不能使用两个智能指针赋值操作,应该使用std::move;而且它可以直接用 if(ptest == NULL) 来判断是否空指针;release、get、reset 等用法也和 auto_ptr 一致,使用函数的返回值赋值时,可以直接使用 =,这里使用c++11 的移动语义特性。另外注意的是当把它当做参数传递给函数时(使用值传递,应用传递时不用这样),传实参时也要使用 std::move,比如foo(std::move(ptest))。它还增加了一个成员函数swap用于交换两个智能指针的值
注意使用reset的时候不要多个智能指针绑定同一普通指针。
share_ptr shared_ptr 多个指针指向相同的对象。shared_ptr使用引用计数,每一个shared_ptr的拷贝都指向相同的内存。每使用他一次,内部的引用计数加1,每析构一次,内部的引用计数减1,减为0时,自动删除所指向的堆内存。shared_ptr内部的引用计数是线程安全的,但是对象的读取需要加锁。
初始化:智能指针是个模板类,可以指定类型,传入指针通过构造函数初始化。也可以使用make_shared函数初始化。不能将指针直接赋值给一个智能指针,一个是类,一个是指针。例如 std::shared_ptr<int> p4 = new int(1)
的写法是错误的
拷贝和赋值:拷贝使得对象的引用计数增加1,赋值使得原对象引用计数减1,当计数为0时,自动释放内存。后来指向的对象引用计数加1,指向后来的对象。
get函数获取原始指针
注意不要用一个原始指针初始化多个shared_ptr,否则会造成二次释放同一内存
注意避免循环引用,shared_ptr的一个最大的陷阱是循环引用,循环,循环引用会导致堆内存无法正确释放,导致内存泄漏。循环引用在weak_ptr中介绍。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 int main () { shared_ptr <Test> ptest (new Test("123" )) ; shared_ptr <Test> ptest2 (new Test("456" )) ; cout <<ptest2->getStr()<<endl ; cout <<ptest2.use_count()<<endl ; ptest = ptest2; ptest->print (); cout <<ptest2.use_count()<<endl ; cout <<ptest.use_count()<<endl ; ptest.reset(); ptest2.reset(); cout <<"done !\n" ; system("pause" ); return 0 ; }
输出:
1 2 3 4 5 6 7 8 9 10 Test creat Test creat456 1 Test delete :123 456 2 2 Test delete :456 done !
weak_ptr shared_ptr依然存在着资源无法释放的问题。看下面这个例子:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 class A ;class B ; class A {public : shared_ptr <B> pointer; A(){ cout << "A creat\n" ; } ~A() { cout << "A 被销毁" << endl ; } };class B {public : shared_ptr <A> pointer; B(){ cout << "B creat\n" ; } ~B() { cout << "B 被销毁" << endl ; } };int fun () { shared_ptr <A> a = make_shared<A>(); shared_ptr <B> b = make_shared<B>(); a->pointer = b; b->pointer = a; cout <<a.use_count()<<endl ; cout <<b.use_count()<<endl ; return 0 ; }int main () { fun(); system("pause" ); return 0 ; }
输出:
1 2 3 4 5 A creat B creat2 2 请按任意键继续. . .
运行结果是 A, B 都不会被销毁,这是因为 a,b 内部的 pointer 同时又引用了 a,b
,这使得 a,b
的引用计数均变为了 2,而离开作用域时,a,b
智能指针被析构,却只能造成这块区域的引用计数减一,这样就导致了 a,b
对象指向的内存区域引用计数不为零,而外部已经没有办法找到这块区域了,也就造成了内存泄露。
解决这个问题的办法就是使用弱引用指针 std::weak_ptr
,std::weak_ptr
是一种弱引用(相比较而言 std::shared_ptr
就是一种强引用)。弱引用不会引起引用计数增加.
std::weak_ptr
没有 *
运算符和 ->
运算符,所以不能够对资源进行操作,它的唯一作用就是用于检查 std::shared_ptr
是否存在,expired()
方法在资源未被释放时,会返回 true
,否则返回 false
。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 class A ;class B ; class A {public : weak_ptr<B> pointer; A(){ cout << "A creat\n" ; } ~A() { cout << "A 被销毁" << endl ; } };class B {public : shared_ptr <A> pointer; B(){ cout << "B creat\n" ; } ~B() { cout << "B 被销毁" << endl ; } };int fun () { shared_ptr <A> a = make_shared<A>(); shared_ptr <B> b = make_shared<B>(); a->pointer = b; b->pointer = a; cout <<a.use_count()<<endl ; cout <<b.use_count()<<endl ; return 0 ; }int main () { fun(); system("pause" ); return 0 ; }
输出:
1 2 3 4 5 6 7 A creat B creat2 1 B 被销毁 A 被销毁 请按任意键继续. . .
原文地址:https://blog.csdn.net/weixin_40721097/article/details/102999973
https://blog.csdn.net/icandoit_2014/article/details/56666277?utm_medium=distribute.pc_relevant.none-task-blog-BlogCommendFromMachineLearnPai2-6.channel_param&depth_1-utm_source=distribute.pc_relevant.none-task-blog-BlogCommendFromMachineLearnPai2-6.channel_param
https://blog.csdn.net/czc1997/article/details/84026887