C++ 智能指针实现

C++ 智能指针实现

C++ 使用智能指针管理资源。C++11 有shared_ptr, weak_ptr, unique_ptr 三种智能指针。unique_ptr 实现比较简单,注意避免拷贝和赋值,要使用移动构造函数进行对象所有权的转移。这里只介绍shared_ptr, weak_ptr 的实现,以及通过 weak_ptr 来避免循环引用的问题。设计主要参考了 msvc 的智能指针实现。这里对其源码做了简化。

0 设计

主要定义了五个类:

  1. MyRefCountBase: 引用计数器的基类,定义了 Destory, Delete_This 两个接口,以及引用计数变量,加减引用计数的方法。
  2. MyRefCount:添加所指向对象的指针,实现Destory, Delete_This 两个接口。
  3. MyBaseSmartPtr:智能指针类型,包含所需成员变量和方法。
  4. MySharedPtr:继承 MyBaseSmartPtr 类型,并调用基类中定义的相应方法来实现该类的行为。
  5. MyWeakPtr:继承 MyBaseSmartPtr 类型,并调用基类中定义的相应方法来实现该类的行为。

类图如下所示:

image

由图可知,所有的指向同一对象的 SharedPtr 和 WeakPtr 共享一份引用计数器。

1 RefCount

引用计数变量与方法

image
如图所示:

  1. m_use 记录使用 sharedptr 引用当前对象的个数, 其初始化值为 1。
  2. m_weak 记录使用 weakptr 引用当前对象的个数,其初始化值为 1。
  3. 红框中的变量和方法要保证是原子操作。
  4. 之所以引用计数值初始化为1,是因为只有其真正拥有一个对象的所有权时,这个 MyRefCountBase 类型对象才会进行初始化。

引用计数的增长和减少

image
上图是实现智能指针的核心逻辑代码。

  1. 当调用 Decref() 方法,会导致 m_use 减 1。如果此时 m_use 为 0,就调用 Destory() 函数,析构其所指向的对象。同时 m_weak 计数减 1。
  2. 当调用 Decwref() 方法,会导致 m_weak 减 1。如果 m_weak 为0,就会调用 Destory_This() 函数,把这个引用计数器对象析构。

2 MyBaseSmartPtr

如下图所示
image
这些函数定义了通过多种方法构造智能指针时引用计数的变化。

  1. Raw_Construct 当我们通过原生指针构造一个智能指针时,首先会 new 一个引用计数器的对象(这个 new 出来的对象会在 Destory_This() 函数中进行销毁),以及为成员变量 m_ptr 赋值。
  2. Move_Construct 移动入参所拥有的对象指针引用计数器指针。因此并不会导致引用计数的变化。
  3. Construct_From_Weak 这个函数会被 MySharedPtr 调用,在拷贝完引用计数器的指针和对象指针后,引用计数 +1。
  4. Weakly_Construct_From 这个函数会被 MyWeakPtr 调用,使用 MySharedPtr/MyWeakPtr 来构造 MyWeakPtr,在拷贝完引用计数器的指针和对象指针后,弱引用计数 +1。

3 SharedPtr

对于 SharedPtr 类型,析构时如何减少引用计数。如下图所示
image

当 MySharedPtr 对象析构时,会调用引用计数器的 Decref() 减少一个引用。

在实现 MySharedPtr 时有一个重要的技巧
image

image
使用 swap 函数进行 reset, 这样原对象会经过 swap 后会变成一个局部变量,当这个对象离开作用域时,就会调用析构函数,使引用计数减少。

4 WeakPtr

对于 WeakPtr 最重要的是在析构时如何减少引用计数。如下图所示
image

5 CircularReference

有如下代码

class B;
class A { // A 拥有一个B的指针
  public:
    A() : m_sptrB(nullptr) {};
    ~A() { std::cout << " A is destroyed" << std::endl; }
    MySharedPtr<B> m_sptrB;
};

class B { // B 拥有一个A的指针
  public:
    B() : m_sptrA(nullptr) {};
    ~B() { std::cout << " B is destroyed" << std::endl; }
    MySharedPtr<A> m_sptrA;
};

TEST(MySmartPtrTest, TEST27) {
    MySharedPtr<B> sptrB(new B);         // 定义一个sptrB变量
    MySharedPtr<A> sptrA(new A);         // 定义一个sptrA变量
    std::cout << sptrA.use_count() << std::endl;  // 1 sptrA
    std::cout << sptrB.use_count() << std::endl;  // 1 sptrB
    sptrB->m_sptrA = sptrA;             // sptA count 2, sptrB拥有B, 申请A.
    sptrA->m_sptrB = sptrB;             // sptB count 2, sptrA拥有A, 申请B.
    std::cout << sptrA.use_count() << std::endl;  // 2
    std::cout << sptrB.use_count() << std::endl;  // 2
    // 所以直到最后, 资源都没有被释放, 因为这两个shared_ptr的引用计数都不是0。
    // 内存泄漏了
}

image
如图所示,当程序结束时,还有 96 字节的内存没有释放。

image
运行结果是 A, B 都不会被销毁,这是因为 a, b 内部的 pointer 同时又引用了 a, b,这使得 a, b 的引
用计数均变为了 2,而离开作用域时,a, b 智能指针被析构,却只能造成这块区域的引用计数减一,这样
就导致了 a, b 对象指向的内存区域引用计数不为零,而外部已经没有办法找到这块区域了,也就造成了
内存泄露。

解决办法是使用弱指针

class B;
class A { // A 拥有一个B的指针
  public:
    A() : m_sptrB(nullptr) {};
    ~A() { std::cout << " A is destroyed" << std::endl; }
    MySharedPtr<B> m_sptrB;
};

class B { // B 拥有一个A的弱指针
  public:
    B() : m_sptrA(nullptr) {};
    ~B() { std::cout << " B is destroyed" << std::endl; }
    MyWeakPtr<A> m_sptrA;
};

TEST(MySmartPtrTest, TEST27) {
    MySharedPtr<B> sptrB(new B);         // 定义一个sptrB变量
    MySharedPtr<A> sptrA(new A);         // 定义一个sptrA变量
    std::cout << sptrA.use_count() << std::endl;  // 1 sptrA
    std::cout << sptrB.use_count() << std::endl;  // 1 sptrB
    sptrB->m_sptrA = sptrA;             // sptA count 1, sptrB拥有B, 申请一个A的弱指针,并不拥有.
    sptrA->m_sptrB = sptrB;             // sptB count 2, sptrA拥有A, 申请B.
    std::cout << sptrA.use_count() << std::endl;  // 1
    std::cout << sptrB.use_count() << std::endl;  // 2
}

image
弱指针不会引起引用计数增加,当换用弱指针的时候,最终的释放流程如上图所示,最后一步只剩下 B,而 B 并没有任何智能指针引用它,因此这块内存资源也会被释放。
其执行流程为:

  1. sptrA 调用 MySharedPtr<A> 的析构函数导致类A对象的引用计数减1,现在已经为0了。
  2. sptrB 调用 MySharedPtr<B> 的析构函数导致类B对象的引用计数减1,现在已经为1了。
  3. 调用 A 的析构函数,打印 " A is destroyed" 字符串。
  4. 调用 MySharedPtr<B> 的析构函数,导致类B对象的引用计数减1,现在为0。
  5. 调用 B 的析构函数,打印 " B is destroyed" 字符串。
  6. 调用 MyWeakPtr<A> 的析构函数,由于 A 的引用计数器已经没有了,所以什么也不做。
    注意!由于除了对引用计数的操作为原子操作,上述部分流程的先后循序并不确定,比如 2,3,4的顺序也可能为 3,4,2。
    运行结果为下图:
    image

posted on 2022-09-21 17:11  LambdaQ  阅读(369)  评论(0编辑  收藏  举报