《白话C++》第10章 STL和boost,Page78 10.4.5 std::shared_ptr

本文探讨了C++中的shared_ptr在多线程场景下的内存管理和引用计数机制,以及它与unique_ptr的区别。通过实例展示了shared_ptr如何解决内存释放问题,并指出GC在并发中的挑战和C++天生的栈内存优势。
摘要由CSDN通过智能技术生成

1. 问题和思路

shared_ptr来自boost,现在已经被C++11收入麾下。

当确实需要在代码中有多处指针指向同一块堆内存时,unique_ptr就用不上了,此时需要使用shared_ptr.

编程语言中指针的引入,很大的一个原因就是需要在代码中的多处(空间或时间上)可以操作同一个对象(同一内存),特别是当程序存在并发时,对同一对象的操作时序也无法保证。而在并发应用中,更是可能在不同的时间线上存在多个指针指向同一处内存。

背后的业务逻辑是存在不同时间,不同地点操作同一数据对象的需求。

单一线程(无并发)的应用中,通常在流程复杂到一定程度时,才会造成指针管理困难。

举个例子,先定义一个变量:ilst,它是一个用于保存指针的容器:

std::list <int *> ilst;

然后有一个Add_1(int* p)函数,它判断入参p指向的整数是不是奇数,如果是,将它加入ilst中;再有一个Add_2(int* p)函数,它判断入参p指向的整数是否是3的倍数,如果是,也加入ilst中。最后main函数

int main()
{
    for(int i = 0; i < 100; ++i)
    {
        int * p = new int(i);
        Add_1(p);
        Add_2(p);
    }
    
    /*此处是围绕ilst的其他处理*/
    
    //最后释放列表中的指针:
    for(auto it = ilst.begin(); it != ilst.end(); ++ it)
    {
        /* 如果一个整数既是奇数,又是3的倍数,则会添加两次,然后再释放两次 */
        delete * it;
    }
}

 出大问题了。

一个整数可能既是奇数,又是3的倍数,所以存在一些指针p被前后加入到ilst中两次。于是在第二个for循环中被释放了两次

既然问题出在内存释放上,能不能使用“std::unique_ptr”解决呢?不行,unique_ptr包装下的指针不能复制,决定了某个指针一旦采用push_back(move(p))的语法加入列表(其中p的类型是unique_ptr <int>),p所拥有的裸指针将变成空指针,后续对它的取值,就是在访问非法内存。

【课堂作业】:感受std::unique_ptr的唯一性

将上例代码中的int*改为std::unique_ptr<int>类型,编译通过后测试运行

说白了,我们就是既要让多个指针共项同一内存,有不想手工释放。因为越是存在多个指针指向多一块内存的情况,就越难以靠程序员的大脑去维护。哪块内存已经没有任何指针直线它(因此千万别忘了释放),哪块内存还有几个指针在指向(因此千万别释放)

要解决这一问题,引入GC(Garbage Collection(垃圾回收))是个办法。为此,程序需要有一个额外的“保姆”线程用于查找,标记哪些堆内存已经不在使用中(没有任何指针指向它),然后释放;而为了保证标记的正确性,该线程在工作时,往往要求程序中的其他线程暂停工作。对程序性能,GC有正面影响,也有负面影响,总体上负面影响巨大

当前C++语言没有GC机制,除性能上的考虑之外,关键还在于引入GC后,会对原有的编程方式影响较大,特别像原有RAII机制中的析构函数调用时机的明确性等。

【重要】:C++天生的GC机制:栈内存

C++支持各种对象在栈上分配内存,而栈的内存可自动回收,因此事实上“栈”是C++语言中天生的性能高效加是一个方便的GC实现。依赖独立线程实现GC的语言,通常只支持内置的简单类型(int,double,boolean等)在栈中分配(会自动回收),复杂对象均需在堆中分配(必须由GC线程回收)。

shared_ptr智能指针,正如其名“shared”可用于管理多个指针共同拥有的内存。

int * pa = new int(123);
int * pb = pa;

pa、pb两个指针共同指向同一块内存,就可以称作pa、pb共享内存。

如果我们通过“pa”释放发所指向的内存,就会造成“pb”指向一块已经被释放的内存,如果随后又想通过“pb”访问那块内存,程序就会运行出错,结果可能悄无声息,也可能程序直接过掉。示例如下:

delete pa;
* pb = 124; ///pb所指向的内存,其实已被收回

///或者,尝试释放
delete pb;

引用计数

shared_ptr的思路是:另弄一个计数,记录待管理的那块内存到底有几个指针指向它,每当有个指针要被释放前,通过计数检查它是不是最后一个指向该内存的指针,如果不是,就不真正释放内存,只是将计数减一,如果是则释放内存。这个“计数”的专业术语叫做“引用计数”

假设有一块内存叫M,然后先后有两个智能指针引用它,我们画四步示意,

首先,智能指针s1创建时,申请了一块内存M,这时计数值为1。

接着,智能指针s2也要引用内存M,于是计数值升为2。

后面,智能指针s1出了作用范围,自动析构,于是计数减1,但内存M并没有被释放

直到s2出了作用范围并析构,计数减为0,M被真正释放。

这其中最重要的一步是第2步,s2创建时,并没有自己申请一块内存,否则,它和s1就井水不犯河水,不存在“共享”了。

而这其中没有提到但最重要的一个实现原理,就是“智能指针”本身其实是一个“栈”对象,不是特指某个智能指针,而是指全部,所以它才会有“出了作用范围”这一说。当新的智能指针对象构造时,会去自增引用计数,析构时则自减引用计数。

2. shared_ptr模拟实现:

#include <iostream>
//当指针不可用,用户又要强行用时,一死了之
#include <cassert>

using namespace std;

//用以记录引用计数
struct MyRefCount
{
    int count;

    MyRefCount()
        : count(1)
    {

    }
};

template <typename T>
class MySharedPtr
{
public:
    //explicit可以防止赋值式构造
    explicit MySharedPtr(T* ptr)
                      //这里创建引用计数
        : _ptr(ptr), _rc(new MyRefCount)
    {

    }
    //拷贝构造, 构造一个新的MySharedPtr
    //构造入参是MySharedPtr引用
    MySharedPtr(MySharedPtr const& sp)
        : _ptr(sp._ptr), _rc(sp._rc)
    {
        ++ (_rc->count);
    }
    //赋值运算符重载
    MySharedPtr & operator = (MySharedPtr const& sp)
    {
        if(& sp == this) ///this是个指针
        {
            return *this;
        }
        //将参数的裸指针和引用计数指针,赋给当前对象
        _ptr = sp._ptr;
        _rc = sp._rc;

        ++ (_rc->count); //引用计数增加1
        return *this; //返回当前对象,即一个新的MySharedPtr
    }

    ~MySharedPtr() //MySharedPtr析构
    {
        --_rc->count;
        //只有当引用计数为0时,
        //才会删除裸指针,同时删除引用计数指针
        if(_rc->count == 0)
        {
            delete _ptr;
            delete _rc;
        }
    }

    T* operator ->()
    {
        if(_rc->count == 0)
        {
            return nullptr;
        }
        //this->a,即为_ptr->a
        return _ptr;
    }

    T& operator * ()
    {
        assert((_rc->count > 0));
        return * _ptr;
    }

    int GetRefCount() const
    {
        assert((_rc->count > 0));
        return _rc->count;
    }

private:
    T* _ptr;  //裸指针
    MyRefCount* _rc; //引用计数
};
void testMySharedPtr()
{
    MySharedPtr <int> sp1(new int);
    cout << "ref count = " << sp1.GetRefCount() << endl; ///

    {
        MySharedPtr <int> sp2(sp1);
        cout << "ref count = " << sp2.GetRefCount() << endl;

        *sp2 = 10;
        cout << "*sp2 = " << *sp2 << endl;
    } //sp2生命周期结束,被析构

    cout << "*sp1 = " << *sp1 << endl;
    cout << "ref count = " << sp1.GetRefCount() << endl;
}

003行产生了一个全新的智能指针sp1,它分配一个整数的内存。现在sp1的引用计数是1

006行,特意嵌入一个子代码块,以便人为地改变变量的作用域。

007行,从sp1拷贝构造出sp2,意味着sp2和sp1各自拥有的裸指针,指向同一块内存。

现在sp2的引用计数是2,sp1的引用计数当然也是2,因为它们各自的_rc指向的引用计数对象是同一个。

010行通过sp2,修改了所指向的整数的值。

012行,内嵌的代码块结束,于是作为栈变量,sp2结束生命周期,被析构。根据推理,此时引用计数减为1,裸指针没有被杀出。

014行,尝试用来证明sp1还正常地活着,我们访问了它的裸指针的内容,确实还是10.

015行输出的当前引用计数,将是1

这个函数结束,sp1也被析构,这次裸指针将被删除。

修改代码,让MySharedPtr持有一个用户自定义类,并尝试输出更加详细的信息。

MySharedPtr在模拟裸指针的行为上,还有许多事需要完善。比如它是并发不安全的,当多个线程同时修改引用计数等操作时,需要对计数操作加锁等。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值