C++智能指针

    C/C++中的堆内存的释放是由程序员自己控制的,但是在某些时候,这些内存的释放时机无法被准确或难以判断出来。智能指针此时应运而生。事实上,有些时候堆内存的释放即使可以被准确判断出来,但是由于编码上的困难或繁琐,也会采用智能指针。

1,C++智能指针的实现原理(简)

    C++ 的智能指针是基于引用计数来实现的。大概原理是以函数栈(一个或多个)内的一个对象(我简单称为指针适配对象,一个或多个)的生命周期去管理同一个引用计数器(堆内int),当该指针适配对象发生构造时计数器初始化为1,当发生赋值、拷贝时计数加1。函数栈内对象随函数的返回而被销毁,此时引用计数器减一,当衰减到0时,销毁该指针适配对象所指向的内存。可以发现,智能指针通过一个或多个栈内对象的生命周期共同管理堆内存的生命周期。
    PS: RAII (Resource Acquisition Is Initialization),也称为“资源获取就是初始化”,是C++语言的一种管理资源、避免泄漏的惯用法。C++标准保证任何情况下,已构造的对象最终会销毁,即它的析构函数最终会被调用。简单的说,RAII 的做法是使用一个对象,在其构造时获取资源,在对象生命期控制对资源的访问使之始终保持有效,最后在对象析构的时候释放资源。

这里写图片描述
    当然以此方法实现的智能指针需要解决以下几个问题:

1,这些smart_ptr instance如何关联到同一个counter上面?
    [SLN] 其实方法有挺多,比如提供一个以counter&(或count*)为参数的智能指针构造函数。这样只要这些smart_ptr instance都是基于这个counter构造的,它们之间自然共享这个counter。但这也带来了一个问题这个counter存在哪里,时函数栈内还是堆内?显然,存在栈内是不行的,因为会随栈销毁。但是如果存在堆内,谁负责销毁这个counter?(虽然只是一个int,很小)。显然最好的方式应该是由最后一个用到counter的smart_ptr instance去销毁它,自然为了避免空悬指针,counter的创建实际也最好在第一个smart_ptr instance创建的时候。事实上C++的shared_ptr正是在构造函数中new counter。自然如果再调用构造函数(普通)构造一个同类型的shared_ptr,两个counter显然不是同一个了。因此不能基于同一指针对象p构造多个shared_ptr。shared_ptr只能通过拷贝、赋值等方式进行传递。每拷贝、或赋值一次,counter计数+1。每个shared_ptr 销栈时,counter计数-1。衰减到0时,它们所指的对象由最后一个shared_ptr对象的析构函数负责释放。

2,智能指针是栈上对象,如何实现类似于普通指针的*和->操作?
    [SLN] 由于智能指针是对象,可以利用C++类的特点,重载operater*和operater->,。这样,智能指针对象就能像普通指针一样进行操作了
3, 智能指针的循环引用问题
    [SLN]使用 weak_ptr,weak_ptr功能上基本等同于shard_ptr但是不会使计数器 +1。

2,C++11智能指针

    C++11基于不同的应用场景,提供了三种类型的智能指针:shared_ptr,unique_ptr,weak_ptr。其中:
    1, shared_ptr允许多个该智能指针共享第“拥有”同一堆分配对象的内存,这通过引用计数实现,会记录有多少个shared_ptr共同指向一个对象,一旦最后一个这样的指针被销毁,也就是一旦某个对象的引用计数变为0,这个对象会被自动删除。
    2, unique_ptr独自享有对象的所有权,同一时刻只能有一个unique_ptr指向给定对象,其拷贝构造,赋值操作已经被直接delete掉了、只提供移动构造函数实现移动语义。由于unique_ptr独享对象所有权,因此它无需引用计数器(因为始终是1)。所持对象随其销毁而销毁。
    3, weak_ptr是为了解决循环引用问题而引入的一种辅助指针。

3,共享智能指针shared_ptr

    shared_ptr使用引用计数,每一个shared_ptr的拷贝都指向相同的内存。每使用他一次,内部的引用计数加1,每析构一次,内部的引用计数减1,减为0时,自动删除所指向的堆内存。shared_ptr内部的引用计数器是线程安全的,但是对象的读取需要加锁。
这里写图片描述
    构造:shared_ptr的构造函数有很多重载版本。这是其中的一种以普通指针为参数的构造方式。

std::shared_ptr<int>  ptra(new int(5));

    初始化:std::shared_ptr ptra = std::make_shared(a);//a是一个对象,而非指针!
个人觉得这里一个不好理解的问题是,智能指针的构造时用的时普通指针,也就是说shared_ptr除了多了内存管理的功能之外,基本上相当于C/C++语言种的“*”操作符;但在初始化(即make_shared)时传入的参数却是对象。这即意味着,将普通指针转为智能指针,如果采用make_shared()的方式,需要作如下处理。

int* p=new int(50);
std::shared_ptr<int> ptra = std::make_shared<int>(*p);//个人理解make_share相当于int* p=&a;里的“&”操作。

    而且此后p只要还没销栈,仍然是指向50的,千万不要再自己free()了;因此如果此时再基于p新建一个智能指针,这两个智能指针的使用计数器是不会智能叠加了!当其中一个计数为0释放了堆内存后,另外一个智能指针内的堆对象指针事实上是被空悬了,运行时将报段错误。
    当然,std::make_shared()主要的运用场合并不是对一个已经存在的堆内存构建智能指针,更多的时候std::make_shared()自己申请空间,此时counter和对象的堆空间可以一起申请和销毁,而不像普通构造函数一样需要分别申请:

Shared_ptr<int> pIn = std::make_shared<int>(5);//构建一个int对象,初始化为5,并管理

    当然普通构造和make_shared各有优缺点,这篇博文做了一些简要分析:https://blog.csdn.net/Jxianxu/article/details/72859800
    拷贝和赋值:td::shared_ptr ptra2(ptra); //copy
    拷贝和赋值构造都会使得使用计数器共享,并且计数器+1;
    shared_ptr.get()函数可以获取原始指针。但此时智能指针仍保留有对堆对象内存管理的权利。

class Parent;
class Child;

class Parent
{
public:
    Parent(){}
    ~Parent() {
        cout << "~Parent()" << endl;
    }
public:
    shared_ptr<Child> m_child;
};

class Child
{
public:
    Child(){}
    ~Child() {
        cout << "~Child()" << endl;
    }
public:
    weak_ptr<Parent> m_parent;//这里为了避免循环引用,比需使用weak_ptr
};



int main()
{
    shared_ptr<Parent> mParent(new Parent());
    shared_ptr<Child> mChild(new Child());

    mParent->m_child = mChild;
    mChild->m_parent = mParent;

    cout << mParent.use_count() << endl;
    cout << mChild.use_count() << endl;

    return 0;
}

如果有需要,也可以自己给智能指针提供计数=0时的deleter,例如shared_ptr 无法智能管理数组,需要加自定义删除操作,否则导致运行错误。如下:

std::shared_ptr<int> sp( new int[10], []( int *p ) { delete[] p; } );
std::shared_ptr<Test> sp(new Test[10], [](Test *p) { delete[] p; });
std::shared_ptr<Test> sp(new Test[10], std::default_delete<Test[]>());
std::shared_ptr<Test[]> sp(new Test[10]);

4,unique_ptr

    unique_ptr: “唯一”拥有其所指对象,同一时刻只能有一个unique_ptr指向给定对象(通过禁止拷贝语义、只提供移动语义而不提供拷贝构造来实现)。unique_ptr由于是独占的,并没有使用计数器,因此如果在某个函数栈内创建了一个unique_ptr,那么等到这个函数销栈,其所指向的堆内存就会释放。从这一点可以看出,如果不使用移动操作,unique_ptr所指向的对内存在效果上相当于函数栈内的栈对象。
但是有一点需要注意的是,unique_ptr指针可以作为函数的返回值返回。即return unique_ptr ptr;是允许的!(这里注意函数栈内对象指针是绝对不允许作为返回值的),即通过unique_ptr的返回,可以把堆内存的控制权移交给父函数。(那能不能作为参数转交给子函数呢?)。
这里写图片描述
*Ps: 在不使用返回值优化(RVO)时,函数内return栈对象需要经过三次构造(不定):
1,函数栈内普通构造
2,返回值作右值时产生的临时对象
3,接着是接收时的拷贝构造
unique_ptr只有移动构造函数,不考虑RVO,它是如何实现返回的呢?似乎以为着在非RVO情况下,第1,2次都是调用的移动构造函数!*

    unique_ptr与shared_ptr不同,unique_ptr可以直接管理数组。当然也可以像shared_ptr一样自定义deleter,所不同的是shared_ptr的Deleter是作为普通的参数传入的,而在unique_ptr种Deleter是作为模板参数传入的!至于这样设计的具体原因,可以查看这篇博文:https://www.cnblogs.com/fuzhe1989/p/7763623.html

std::unique_ptr<Test[]> sp(new Test[10]);
std::unique_ptr<Test,> sp(new Test[10], [](Test *p) { delete[] p; });//错误

auto myDeleter = [](Test *p) { delete[] p; };
std::unique_ptr<Test, decltype(myDeleter)> sp(new Test[10], myDeleter);

void Deleter(Test* obj)
{
    delete[] obj;
};
std::unique_ptr<Test, decltype(Deleter)*> sp(new Test[10], Deleter);


class DeleterC
{
public:
    void operator() (Test* obj)
    {
        delete[] obj;
    }
};
std::unique_ptr<Test, DeleterC> up1(new Test[10]);
std::unique_ptr<Test, DeleterC> up2(new Test[10], up1.get_deleter());

5,weak_ptr

    weak_ptr也是一个引用计数型智能指针,但它不增加对象的引用计数,是一种弱引用。与之相对,shared_ptr是强引用,只要有一个指向对象的shared_ptr存在,该对象就不会析构,直到指向对象的最后一个shared_ptr析构或reset()时才会被销毁。
这里写图片描述
    weak_ptr可以解决空悬指针和循环引用问题。事实上这两个问题并不难理解。
    空悬问题是指两个指针p1,p2指向同一块内存,假设p1在某一时刻释放了,但p2没有发生变化,并且仍旧指向这块已经被释放掉的内存,导致p2空悬。weak_ptr由于可以作expired()检测,或判断weak_ptr.lock()==null(内存如果已经将会获得一个空的shared_ptr),避免了访问空悬指针。这里有一点需要注意的是weak_ptr无法单独存在,其只能由shared_ptr作为参数构造,或由另一个weak_ptr构造。
    循环引用是指一个智能指针shared_ptr< A>内堆对象a中包含另一个成员变量shared_ptr< B> b,而b中又直接或间接包含成员变量shared_ptr< A> a。造成相互依赖,此时计数器永远无法衰减为0;如下图所示。
这里写图片描述
    weak_ptr不会增加引用计数,因此,解决循环引用的方法只需将其中的一个shared_ptr换成weak_ptr即可。如下图。
这里写图片描述

5,智能指针的使用场景

    C++的智能指针从书写上来讲,实在太丑了。并且根据不少人的经验,智能指针的传染性很强,一处用了智能指针,很多地方都要继续用智能指针!并且由于智能指针会使人对于内存的释放麻痹大意。事实上如果不恰当的使用智能指针有时候会在很不起眼的地方造成内存泄漏。另一个缺点就是,潜在的循环引用有时候难以发现!
因此个人的观点是:对于不太复杂的堆内存优先使用普通指针管理,如果管理逻辑复杂或代码繁杂再考虑用智能指针。并且优先考虑使用unique_ptr,慎用shared_ptr。

  • 2
    点赞
  • 3
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值