【重学C++】【指针】手撕shared_ptr:源码阅读及从零开始亲自动手写一个自己的智能指针

大家好,我是 同学小张,持续学习C++进阶知识AI大模型应用实战案例,持续分享,欢迎大家点赞+关注,共同学习和进步。

重学C++系列文章,在会用的基础上深入探讨底层原理和实现,适合有一定C++基础,想在C++方向上持续学习和进阶的同学。争取让你每天用5-10分钟,了解一些以前没有注意到的细节。


今天我们首先阅读一下shared_ptr的源码,然后亲自动手,从零开始实现一个自己的智能指针,从而对智能指针的底层原理有一个更深入的理解。

1. shared_ptr源码略读

// 源码中的 shared_ptr 类定义,继承自 __shared_ptr 类
class shared_ptr : public __shared_ptr<_Tp>

shared_ptr的源码基本都在 __shared_ptr 类中,这是 shared_ptr 的基本实现。

1.0 成员变量

两个成员变量,一个是原始指针,一个是引用计数。

element_type *_M_ptr;            // Contained pointer.
__shared_count<_Lp> _M_refcount; // Reference counter.

1.1 构造函数

以默认构造函数为例展示,代码很简单,不过多解释。

constexpr __shared_ptr() noexcept
        : _M_ptr(0), _M_refcount()
{
}

1.2 赋值运算符重载

复制赋值:

template<typename _Yp>
_Assignable<_Yp> operator=(const __shared_ptr<_Yp, _Lp>& __r) noexcept
{
    _M_ptr = __r._M_ptr;
    _M_refcount = __r._M_refcount; // __shared_count::op= doesn't throw
    return *this;
}

移动赋值:

__shared_ptr&
operator=(__shared_ptr&& __r) noexcept
{
    __shared_ptr(std::move(__r)).swap(*this);
    return *this;
}

1.3 其它成员函数

1.3.1 reset函数

多种reset方法,最常用的是下面这两种,一种直接reset到空,一种reset到指定原始指针。

void reset() noexcept
{ __shared_ptr().swap(*this); }

template<typename _Yp>
_SafeConv<_Yp> reset(_Yp* __p) // _Yp must be complete.
{
    // Catch self-reset errors.
    __glibcxx_assert(__p == nullptr || __p != _M_ptr);
    __shared_ptr(__p).swap(*this);
}

1.3.2 get函数

element_type* get() const noexcept
{ return _M_ptr; }

1.4 引用计数的实现

可能大家看上面的源码也发现了,拷贝构造和复制赋值运算时,引用计数都是直接拷贝的,并没有看到引用计数的增加。其实,增加是在引用计数类里面实现的。

增加引用计数的实现如下:

void _M_add_ref_copy()
      { __gnu_cxx::__atomic_add_dispatch(&_M_use_count, 1); }

这里注意 atomic方式增加,原子操作,所以对于智能指针的引用计数来说,是线程安全的。

看下它什么时候会被调用,即什么情况下会增加引用计数:

 __shared_count(const __shared_count& __r) noexcept
    : _M_pi(__r._M_pi)
{
    if (_M_pi != nullptr)
        _M_pi->_M_add_ref_copy();
}

__shared_count& operator=(const __shared_count& __r) noexcept
{
    _Sp_counted_base<_Lp>* __tmp = __r._M_pi;
    if (__tmp != _M_pi)
        {
        if (__tmp != nullptr)
            __tmp->_M_add_ref_copy();
        if (_M_pi != nullptr)
            _M_pi->_M_release();
        _M_pi = __tmp;
        }
    return *this;
}

所以,在拷贝构造和复制赋值运算时,引用计数会加1。

那什么时候减少引用计数和销毁这块内存呢?

先看一下其构造函数,可以接收一个原始指针。所以这块内存,在引用计数类中是知道的。

template<typename _Ptr> explicit
__shared_count(_Ptr __p) : _M_pi(0)
{
    ...
}

释放和减少引用计数在这:

~__shared_count() noexcept
{
	if (_M_pi != nullptr)
	  _M_pi->_M_release();
}

template<>
inline void _Sp_counted_base<_S_single>::_M_release() noexcept
{
    if (--_M_use_count == 0)
    {
        _M_dispose();
        if (--_M_weak_count == 0)
            _M_destroy();
    }
}

析构时,原始指针调用 _M_release函数,减少引用计数。如果引用计数为0,则调用 _M_dispose函数,释放引用计数。

思考:这里为什么要在引用计数里面实现加减和释放原始内存的操作?为什么不在智能指针的类里面实现?

好了,源码大体看到这,主要看了下部分构造函数,以及引用计数的增加、减少以及原始内存的销毁。这也是智能指针中最核心的部分。下面按我们自己的思路,实现一个智能指针。

2. 手撕shared_ptr

2.0 成员变量

先把成员变量定义了。我们都知道,shared_ptr的成员变量有2个,一个用来保存原始指针,一个用来保存引用计数。

template<typename T>
class MySharedPtr
{

private:
    T* m_ptr;
    SharedCount* m_ref_count;
};

其中引用计数,我们也按照源码那样,定义一个类:

class SharedCount
{
public:
    SharedCount() : m_count(1) {}
    ~SharedCount() {}
    void addCount() { ++m_count; }
    long getCount() { return m_count; }
    long reduceCount() { return --m_count; }

private:
    long m_count;
};

SharedCount初始值应该为1。

2.1 构造函数

2.1.1 默认/有值构造函数

默认构造或从原始指针构造一个智能指针。使用原始指针构造时,需要初始化一个引用计数。

MySharedPtr(T* ptr = nullptr)
{
    m_ptr = ptr;
    if (m_ptr) // 如果初始化指向了一块内存,则需要初始化一个引用计数,刚开始是1
    {
        m_ref_count = new SharedCount();
    }
}

2.1.2 拷贝构造函数

拷贝构造函数,要考虑两个成员变量:原始指针和引用计数的拷贝过程。

首先,原始指针直接拷贝即可。

然后,需要判断下原始指针是否有效,如果无效,也没必要有引用计数。

如果有效,则拷贝引用计数,同时,因为这块内存区域多了一个指针,所以要增加一下引用计数。

MySharedPtr(MySharedPtr<T>& ptr)
{
    m_ptr = ptr.m_ptr;
    if (m_ptr)
    {
        m_ref_count = ptr.m_ref_count;
        m_ref_count->addCount();
    }
}

2.1.3 移动构造函数

移动构造函数,是与原指针交换资源的所有权。首先将原始指针拷贝下来,然后将引用计数也拷贝下来。

然后,因为交换所有权,所以传进来的智能指针的原始指针和引用计数都要置为空。

并不存在增加指向内存的指针,所以引用计数不变。

MySharedPtr(MySharedPtr<T>&& ptr) noexcept
{
    m_ptr = ptr.m_ptr;
    if (m_ptr)
    {
        m_ref_count = ptr.m_ref_count;
        ptr.m_ptr = nullptr;
        ptr.m_ref_count = nullptr;
    }
}

2.2 赋值运算符重载

赋值运算符重载,与拷贝构造函数类似,都是将传入的智能指针中的原始指针和引用计数拷贝过来。同时引用计数需要增加。

2.2.1 拷贝赋值

MySharedPtr& operator=(MySharedPtr<T>& ptr)
{
    m_ptr = ptr.m_ptr;
    if (m_ptr)
    {
        m_ref_count = ptr.m_ref_count;
        m_ref_count->addCount();
    }
    return *this;
}

2.2.2 移动赋值

MySharedPtr& operator=(MySharedPtr<T>&& ptr) noexcept
{
    m_ptr = ptr.m_ptr;
    if (m_ptr)
    {
        m_ref_count = ptr.m_ref_count;
        ptr.m_ptr = nullptr;
        ptr.m_ref_count = nullptr;
    }
    return *this;
}

2.3 析构函数

析构函数,关注两个成员变量都是原始指针类型,内存都是在堆上new出来的,都需要手动释放。

在销毁这个智能指针时,判断下引用计数是否是0,!m_ref_count->reduceCount(),如果是0,则说明这块内存区域已经没有指针指向了,可以释放掉。

~MySharedPtr()
{
    if (m_ptr && m_ref_count && !m_ref_count->reduceCount())
    {
        delete m_ptr;
        delete m_ref_count;
    }
}

2.4 指针特性运算符重载


// -> 运算符重载
T* operator->()
{
    return m_ptr;
}

// * 运算符重载
T& operator*()
{
    return *m_ptr;
}

2.5 其它成员函数

2.5.1 get函数

获取原始指针

T* get()
{
    return m_ptr;
}

2.5.2 uinque函数

判断是否是独占的,也就是引用计数是否为1。

bool unique()
{
    return m_ptr && m_ref_count && (m_ref_count->getCount() == 1);
}

2.5.3 swap函数

交换两个智能指针的原始指针和引用计数。

void swap(MySharedPtr<T>& sptr){
    std::swap(m_ptr, sptr.m_ptr);
    std::swap(m_ref_count, sptr.ref_count);
}

2.5.4 use_count函数

获取引用计数。

long use_count()
{
    if (m_ptr && m_ref_count)
    {
        return m_ref_count->getCount();
    }
    return 0;
}

2.5.5 reset函数

void reset(T* ptr = nullptr)
{
    MySharedPtr<T>(ptr).swap(*this);
}

2.6 测试

测试代码:

MySharedPtr<int> p1 = MySharedPtr<int>(new int(10));
std::printf("p1 use count = %d\n", p1.use_count());
MySharedPtr<int> p2 = p1;
std::printf("p2 use count = %d\n", p2.use_count());
std::printf("p2 = %d\n", *p2);

int *p4 = new int(30);
p2.reset(p4);
std::printf("p2 use count = %d\n", p2.use_count());
std::printf("p2 = %d\n", *p2);

2.7 完整代码

#include <iostream>

class SharedCount
{
public:
    SharedCount() : m_count(1) {}
    ~SharedCount() {}
    void addCount() { ++m_count; }
    long getCount() { return m_count; }
    long reduceCount() { return --m_count; }

private:
    long m_count;
};

template<typename T>
class MySharedPtr
{
public:
    // 构造函数
    MySharedPtr(T* ptr = nullptr) : m_ptr(ptr), m_ref_count(nullptr)
    {
        std::printf("MySharedPtr 构造函数 is called\n");
        if (m_ptr) // 如果初始化指向了一块内存,则需要初始化一个引用计数,刚开始是1
        {
            m_ref_count = new SharedCount();
        }
    }

    // 拷贝构造函数
    MySharedPtr(MySharedPtr<T>& ptr)
    {
        std::printf("MySharedPtr 拷贝构造函数 is called\n");
        m_ptr = ptr.m_ptr;
        if (m_ptr)
        {
            m_ref_count = ptr.m_ref_count;
            m_ref_count->addCount();
        }
    }

    MySharedPtr(MySharedPtr<T>&& ptr) noexcept
    {
        std::printf("MySharedPtr 移动构造函数 is called\n");
        m_ptr = ptr.m_ptr;
        if (m_ptr)
        {
            m_ref_count = ptr.m_ref_count;
            ptr.m_ptr = nullptr;
            ptr.m_ref_count = nullptr;
        }
    }

    // 赋值运算符
    MySharedPtr& operator=(MySharedPtr<T>& ptr)
    {
        std::printf("MySharedPtr 拷贝赋值 is called\n");
        m_ptr = ptr.m_ptr;
        if (m_ptr)
        {
            m_ref_count = ptr.m_ref_count;
            m_ref_count->addCount();
        }
        return *this;
    }

    MySharedPtr& operator=(MySharedPtr<T>&& ptr) noexcept
    {
        std::printf("MySharedPtr 移动赋值 is called\n");
        m_ptr = ptr.m_ptr;
        if (m_ptr)
        {
            m_ref_count = ptr.m_ref_count;
            ptr.m_ptr = nullptr;
            ptr.m_ref_count = nullptr;
        }
        return *this;
    }

    ~MySharedPtr()
    {
        std::printf("MySharedPtr 析构函数 is called\n");
        if (m_ptr && m_ref_count && !m_ref_count->reduceCount())
        {
            delete m_ptr;
            delete m_ref_count;
        }
    }

    T* operator->()
    {
        std::printf("MySharedPtr -> is called\n");
        return m_ptr;
    }
    T& operator*()
    {
        std::printf("MySharedPtr * is called\n");
        return *m_ptr;
    }

    T* get()
    {
        std::printf("MySharedPtr get is called\n");
        return m_ptr;
    }

    long use_count()
    {
        std::printf("MySharedPtr use_count is called\n");
        if (m_ptr && m_ref_count)
        {
            return m_ref_count->getCount();
        }
        return 0;
    }

    bool unique()
    {
        std::printf("MySharedPtr unique is called\n");
        return m_ptr && m_ref_count && (m_ref_count->getCount() == 1);
    }

    void swap(MySharedPtr<T>& sptr){
        std::printf("MySharedPtr swap is called\n");
        std::swap(m_ptr, sptr.m_ptr);
        std::swap(m_ref_count, sptr.m_ref_count);
    }

    void reset(T* ptr = nullptr)
    {
        std::printf("MySharedPtr reset is called\n");
        MySharedPtr<T>(ptr).swap(*this);
    }

private:
    T* m_ptr;
    SharedCount* m_ref_count;
};

int main(int argc, char *argv[])
{
    MySharedPtr<int> p1 = MySharedPtr<int>(new int(10));
    std::printf("p1 use count = %d\n", p1.use_count());
    MySharedPtr<int> p2 = p1;
    std::printf("p2 use count = %d\n", p2.use_count());
    std::printf("p2 = %d\n", *p2);

    int *p4 = new int(30);
    p2.reset(p4);
    std::printf("p2 use count = %d\n", p2.use_count());
    std::printf("p2 = %d\n", *p2);
    
    return 0;
}

运行结果:

MySharedPtr 构造函数 is called
MySharedPtr use_count is called
p1 use count = 1
MySharedPtr 拷贝构造函数 is called
MySharedPtr use_count is called
p2 use count = 2
MySharedPtr * is called
p2 = 10
MySharedPtr 构造函数 is called
MySharedPtr reset is called
MySharedPtr 构造函数 is called
MySharedPtr swap is called
MySharedPtr 析构函数 is called
MySharedPtr use_count is called
p2 use count = 1
MySharedPtr * is called
p2 = 30
MySharedPtr 析构函数 is called
MySharedPtr 析构函数 is called
MySharedPtr 析构函数 is called

3. 总结

本文我们首先阅读了shared_ptr的源码,然后重点解释了其引用计数和内存释放等部分的实现。然后按照我们自己的理解,亲自动手从零实现了一个智能指针,从而对智能指针shared_ptr有了更深入的理解。

如果觉得本文对你有帮助,麻烦点个赞和关注呗 ~~~


  • 大家好,我是 同学小张,持续学习C++进阶知识AI大模型应用实战案例
  • 欢迎 点赞 + 关注 👏,持续学习持续干货输出
  • +v: jasper_8017 一起交流💬,一起进步💪。
  • 微信公众号也可搜同学小张 🙏

本站文章一览:

在这里插入图片描述

最近更新

  1. TCP协议是安全的吗?

    2024-05-12 14:54:03       16 阅读
  2. 阿里云服务器执行yum,一直下载docker-ce-stable失败

    2024-05-12 14:54:03       16 阅读
  3. 【Python教程】压缩PDF文件大小

    2024-05-12 14:54:03       15 阅读
  4. 通过文章id递归查询所有评论(xml)

    2024-05-12 14:54:03       18 阅读

热门阅读

  1. linux内核debug(一)oops

    2024-05-12 14:54:03       11 阅读
  2. 户外景区亲子儿童剧本杀小程序系统开发搭建

    2024-05-12 14:54:03       8 阅读
  3. SQL手册

    2024-05-12 14:54:03       11 阅读