智能指针 auto_ptr, unque_ptr, shared_ptr, weak_ptr

来源:互联网 发布:xcalibur软件下载 编辑:程序博客网 时间:2024/05/21 18:40

参考: http://www.jellythink.com/archives/684

http://www.cnblogs.com/lanxuezaipiao/p/4132096.html


1、什么是智能指针?

C++程序设计中使用堆内存是非常频繁的操作,堆内存的申请和释放都由程序员自己管理。
程序员自己管理堆内存可以提高了程序的效率,但是整体来说堆内存的管理是麻烦的,
C++11中引入了智能指针的概念,方便管理堆内存。使用普通指针,容易造成堆内存泄露(忘记释放),
二次释放,程序发生异常时内存泄露等问题等,使用智能指针能更好的管理堆内存。

理解智能指针需要从下面三个层次:
从较浅的层面看,智能指针是利用了一种叫做RAII(资源获取即初始化)的技术对普通的指针进行封装,
这使得智能指针实质是一个对象,行为表现的却像一个指针。
智能指针的作用是防止忘记调用delete释放内存和程序异常的进入catch块忘记释放内存。
另外指针的释放时机也是非常有考究的,多次释放同一个指针会造成程序崩溃,这些都可以通过智能指针来解决。
智能指针还有一个作用是把值语义转换成引用语义。C++和Java有一处最大的区别在于语义不同,在Java里面下列代码:
Animal a = new Animal();
Animal b = a;
    你当然知道,这里其实只生成了一个对象,a和b仅仅是把持对象的引用而已。但在C++中不是这样,
    Animal a;
    Animal b = a;
    这里却是就是生成了两个对象。

2、智能指针引入:

为什么需要智能指针呢?

void remodel(std::string & str){std::string *ps = new std::string(str);...str = ps;return;}//上述代码会造成内存泄露。所以需要在return之前加入代码: delete ps;然而却有可能很大程度上忘记void remodel(std::string & str){std::string *ps = new std::string(str);...if(..)throw exception();str = ps;delete ps;return;}
当异常发生时,上面delete不会被执行,同样会内存泄露。这时候我们就需要智能指针了。这时我们会想:当remodel这样的函数终止(不管是正常终止,还是由于出现了异常而终止),本地变量都将自动从栈内存中删除—因此指针ps占据的内存将被释放,如果ps指向的内存也被自动释放,那该有多好啊。

我们知道析构函数有这个功能。如果ps有一个析构函数,该析构函数将在ps过期时自动释放它指向的内存。

但ps的问题在于,它只是一个常规指针,不是有析构函数的类对象指针。如果它指向的是对象,则可以在对象过期时,让它的析构函数删除指向的内存。

这正是 auto_ptr、unique_ptr和shared_ptr这几个智能指针背后的设计思想。我简单的总结下就是:将基本类型指针封装为类对象指针(这个类肯定是个模板,以适应不同基本类型的需求),并在析构函数里编写delete语句删除指针指向的内存空间。

3、智能指针有哪些?使用时需要:

STL一共给我们提供了四种智能指针:auto_ptr、unique_ptr、shared_ptr和weak_ptr。
模板auto_ptr是C++98提供的解决方案,C+11已将将其摒弃,这里不予讨论;
使用时
(1)仅需要#include<memory>;
(2)并定义相关智能指针对象代替普通指针;
(3)可以将new获得的地址复制给智能指针。当智能指针过期时,其析构函数将使用delete来释放内存。
无需记住稍后释放这些内存,在智能指针过期时,这些内存将自动被释放。 

4、探索c++ 11中三个智能指针

关于unique_ptr,shared_ptr,weak_ptr:对于编译器来说,智能指针实际上是一个栈对象,并非指针类型,在栈对象生命期即将结束时,智能指针通过析构函数释放有它管理的堆内存。所有智能指针都重载了“operator->”操作符,直接返回对象的引用,用以操作对象。访问智能指针原来的方法则使用“.”操作符。

访问智能指针包含的裸指针则可以用 get() 函数。由于智能指针是一个对象,所以if (my_smart_object)永远为真,要判断智能指针的裸指针是否为空,需要这样判断:if (my_smart_object.get())。智能指针包含了 reset() 方法,如果不传递参数(或者传递 NULL),则智能指针会释放当前管理的内存。如果传递一个对象,则智能指针会释放当前对象,来管理新传入的对象。

智能指针类都有一个explicit构造函数 此时,还有需要注意的地方,我们可以看一下auto_ptr如何定义的:

template<class X> class auto_ptr{public:explicit auto_ptr(X *p = 0) throw();...};

显而易见,使用了explicit关键字。 
explicit关键字只能用于修饰只有一个参数的类的构造函数,它的作用是表面该构造函数是显示的,而非隐式的。
可以阻止不应该允许的经过转换构造函数进行的隐式转换的发生。
因此,请看下面的代码:

shared_ptr<double>pd;double *p_reg = new double;pd = p_reg;//错误,不允许隐式转换pd = shared_ptr<double>(p_reg);//正确shared_ptr<double>pshared = p_reg;//错误,不允许隐式转换shared_ptr<double>pshared(p_reg);//正确
智能指针很多方面都类似常规指针 ,ps是一个智能指针的对象,可以对他执行解除引用操作(*ps)、用它来访问结构成员(ps->puffIndex)、将它赋给指向相同类型的常规指针。切忌把智能指针用于非堆内存

string vacation("hello world");

shared_ptr<string>pvac(&vacation);
当pvac过期时,程序将把delete运算符用于非堆内存,将导致错误。

(1)auto_ptr:
  转移资源。原理和操作方法如上所说,但是存在这种情况: 
int a=10; 
int *p=&a; 
auto_ptr p1(p); 
auto_ptr p2(p1); 
//此时,p1这个智能指针就无法管理p这块内存内存空间了,因为在auto_ptr的拷贝构造函数中是这样实现的: 
auto_ptr(auto_ptr& p) 
:_ptr(p._ptr) 
{ 
  _ptr=NULL; 

这种情况下,原来的智能指针p1就无法再对p操作,并且在最终函数结尾,执行析构函数的时候,
还会对p1进行析构,即使此时p1保存的是一个空指针,但是释放造成的开销也是不必要的。
并且:auto_ptr存在以下几个缺陷:

1>不要使用auto_ptr保存一个非动态开辟空间的指针,因为在作用域结束的时候,会执行智能指针的析构函数,释放这块空间,但非动态的空间又无法释放;
//正确情况:
int i=new int(1);   //堆上的空间——动态开辟
auto_ptr<int> ap1(&i);
//错误情况:
int i=1;  //栈上的空间
auot_ptr<int> ap2(&i);
2>不要使用两个auto_ptr指针指向同一个指针,具体原因上面解释过; 
3>不要使用auto_ptr指向一个指针数组,因为auto_ptr的析构函数所用的是delete而不是delete[],不匹配; 
4>不要将auto_ptr储存在容器中,因为赋值和拷贝构造后原指针无法使用。 
 还有最重要的一点就是,什么情况下也别使用auto_ptr智能指针。



(2)unique_ptr
unique_ptr“唯一”拥有其所指对象,同一时刻只能有一个unique_ptr指向给定对象(通过禁止拷贝语义、只有移动语义来实现)。
相比与原始指针unique_ptr用于其RAII的特性,使得在出现异常的情况下,动态资源能得到释放。unique_ptr指针本身的生命周期:
从unique_ptr指针创建时开始,直到离开作用域。离开作用域时,若其指向对象,则将其所指对象销毁(默认使用delete操作符,用户可指定其他操作)。
unique_ptr指针与其所指对象的关系:在智能指针生命周期内,可以改变智能指针所指对象,如创建智能指针时通过构造函数指定、
通过reset方法重新指定、通过release方法释放所有权、通过移动语义转移所有权。

#include <iostream>#include <memory>int main() {{std::unique_ptr<int> uptr(new int(10));  //创建时绑定动态对象//std::unique_ptr<int> uptr2 = uptr;  //不能賦值//std::unique_ptr<int> uptr2(uptr);  //不能拷貝std::unique_ptr<int> uptr2 = std::move(uptr); //轉換所有權uptr2.release(); //释放所有权std::unique_ptr<int>uptr3;uptr3.reset(new int (3));  //通过reset方法重新指定}//超過uptr的作用域,內存釋放}
(3)shared_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中介绍。

#include <iostream>#include <memory>int main() {{int a = 10;std::shared_ptr<int> ptra = std::make_shared<int>(a);std::shared_ptr<int> ptra2(ptra); //copystd::cout << ptra.use_count() << std::endl;int b = 20;int *pb = &a;//std::shared_ptr<int> ptrb = pb;  //errorstd::shared_ptr<int> ptrb = std::make_shared<int>(b);ptra2 = ptrb; //assignpb = ptrb.get(); //获取原始指针std::cout << ptra.use_count() << std::endl;std::cout << ptrb.use_count() << std::endl;}}
注意:std::shared_ptr<int>p(new int (4));
auto p1=std::make_shared<int>(4); //这里p和p1是等价的,但是make_shared<T>是一个非成员函数,使用的好处是可以一次分配共享对象和智能指针自身的内存,
而显式调用shared_ptr构造函数来构造至少需要两次分配内存,出了产生额外的开销,可能还会导致内存泄漏(比如,如果内存分配失败就无法释放内存引起内存泄漏)。


多线程中:线程安全
关于多线程中使用shared_ptr,有如下几点描述:


1. 同一个shared_ptr被多个线程读,是线程安全的;
2. 同一个shared_ptr被多个线程写,不是 线程安全的;
3. 共享引用计数的不同的shared_ptr被多个线程写,是线程安全的。 对于第一点,没有什么说的;
对于第二点,同一个shared_ptr在不同的线程中进行写操作不是线程安全的,
那基于第三点,我们一般会有以下方案来实现线程安全:对于线程中传入的外部shared_ptr对象,
在线程内部进行一次新的构造,例如: sharedptr AObjTmp = outerSharedptrObj;


(4)weak_ptr
weak_ptr是为了配合shared_ptr而引入的一种智能指针,因为它不具有普通指针的行为,
没有重载operator*和->,它的最大作用在于协助shared_ptr工作,像旁观者那样观测资源的使用情况。
weak_ptr可以从一个shared_ptr或者另一个weak_ptr对象构造,获得资源的观测权。但weak_ptr没有共享资源,
它的构造不会引起指针引用计数的增加。使用weak_ptr的成员函数use_count()可以观测资源的引用计数,
另一个成员函数expired()的功能等价于use_count()==0,但更快,表示被观测的资源(也就是shared_ptr的管理的资源)已经不复存在。
weak_ptr可以使用一个非常重要的成员函数lock()从被观测的shared_ptr获得一个可用的shared_ptr对象, 
从而操作资源。但当expired()==true的时候,lock()函数将返回一个存储空指针的shared_ptr。

#include <iostream>#include <memory>int main() {{std::shared_ptr<int> sh_ptr = std::make_shared<int>(10);std::cout << sh_ptr.use_count() << std::endl;std::weak_ptr<int> wp(sh_ptr);std::cout << wp.use_count() << std::endl;if(!wp.expired()){std::shared_ptr<int> sh_ptr2 = wp.lock(); //get another shared_ptr*sh_ptr = 100;std::cout << wp.use_count() << std::endl;}}//delete memory}#include <iostream>#include <string>#include <memory>using namespace std;int main(){auto_ptr<string>films[5] = {auto_ptr<string> (new string("Fowl Balls")),auto_ptr<string> (new string("Duck Walks")),auto_ptr<string> (new string("Chicken Runs")),auto_ptr<string> (new string("Turkey Errors")),auto_ptr<string> (new string("Goose Eggs"))};auto_ptr<string> pwin;pwin = films[2]; // films[2] loses ownership. 将所有权从 films[2]转让给pwin,此时films[2]不再引用该字符串从而变成空指针//unique_ptr<string> pwin; //编译错误//pwin = films[2];//shared_ptr<string> pwin; //没有问题!//pwin = films[2];cout << "The nominees for best avian baseballl film are\n";for(int i = 0; i < 5; ++i)cout << *films[i] << endl;cout << "The winner is " << *pwin << endl;cin.get();return 0;}

当使用shared_ptr的时候,pwin和films[2]指向同一个对象,而引用计数从1增加到了2.在程序的末尾,pwin首先调用其析构函数,该析构函数将引用计数降低到1,所以不会有问题。 即 

使用auto_ptr 所有权转让 运行将会有错误 

使用shared_ptr 指向同一对象,引用计数增加,不会有问题 

使用unique_ptr 采用所有权模型,编译就会有错误

C++11中的unique_ptr是auto_ptr的替代品,它与auto_ptr一样拥有唯一拥有权的特性,与auto_ptr不一样的是,unique_ptr是没有复制构造函数的,这就防止了一些“悄悄地”丢失所有权的问题发生,如果需要将所有权进行转移,使用move
因此我们也可以看出unique_ptr优于auto_ptr的一个原因: 
unique_ptr比auto_ptr更安全 
还有一个原因就是: 
auto_ptr只能与new一起使用,不能与new[]一起使用(share_ptr同样) 
unique_ptr可以使用new,也可以使用new[] 

如下面所示: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;}//但是unque_ptr可以直接可以使用new,也可以使用new[],  如:unique_ptr<A[]> arrayObj(new A[5]);

5、如何选择智能指针

(1)如果程序要使用多个指向同一个对象的指针,应选择shared_ptr 
这样的情况包括: 
有一个指针数组,并使用一些辅助指针来标示特定的元素,如最大的元素和最小的元素; 
两个对象包含都指向第三个对象的指针; 
STL容器包含指针。 
很多STL算法都支持复制和赋值操作,这些操作可用于shared_ptr,但不能用于unique_ptr(编译器发出warning)和auto_ptr(行为不确定)。
如果你的编译器没有提供shared_ptr,可使用Boost库提供的shared_ptr。 
(2)如果程序不需要多个指向同一个对象的指针,则可使用unique_ptr。如果函数使用new分配内存,
并返还指向该内存的指针,将其返回类型声明为unique_ptr是不错的选择。这样,所有权转让给接受返回值的unique_ptr,
而该智能指针将负责调用delete。可将unique_ptr存储到STL容器在那个,只要不调用将一个unique_ptr复制或赋给另一个算法(如sort())

阅读全文
1 0
原创粉丝点击