行行宜行行

热爱风光、热爱新技术、踏出每一步,过好每一天!

深入理解C++智能指针

智能指针是C++11引入的一大特性,解决了以前完全依赖手动管理内存,一不小心就出现内存泄漏、悬垂指针和二次释放的问题,本文结合自己复习过程中的理解,分析智能指针的用法、底层结构以及原理。

一、为什么要引入智能指针

原始的做法

在引入智能指针之前,以前基本上都是靠自己手动管理内存的,并且要时刻记得 malloc , free, new, delete的配对使用,一旦使用错误就会造成严重的后果,这都是十分头疼的事情。比如下面的例子:

#include <iostream>

int main() {
    int* wild_ptr;		/// 这里没有初始化
    
    /// 访问野指针指向的内存,结果是未定义的
    std::cout << *wildPtr << std::endl;  // 可能崩溃或输出垃圾值
    
    int * ptr = new int(10);
    delete ptr;
    
    /// 上方已经释放了ptr指针,现在继续访问会导致未定义行为
    std::cout << *ptr << std::endl;
    
    /// double free也会导致未定义行为
    delete ptr;
   
    return 0;
}

以及还有常见的在函数或者成员函数中使用完毕,但是却忘记释放,都会引发内存问题。

引入智能指针的核心目标

能够实现 “资源获取即初始化(Resource Acquisition Is InitializationRAII)”,将资源与对象的生命周期绑定,能够自动管理内存的生命周期,减少人为操作导致的失误。

二、智能指针基础介绍

核心原理

RAII

RAII表示资源获取与对象的构造进行绑定,在对象的生命周期结束时,释放对应的资源,通过类封装原始指针,在析构函数中执行释放操作实现RAII的行为。

所有权模型

智能指针的核心是通过“所有权”机制对对象进行管理,不同的智能指针的所有权的策略不同,常见的策略有独占、共享等。

智能指针的分类

智能指针可以分为侵入式智能指针与非侵入式智能指针,核心不同在于控制块(`control block) 由谁“提供”。

侵入式智能指针

对象自己包含了引用计数变量,智能指针只负责调用引用计数的加减,不持有控制块。

主要特点有

  1. 引用计数直接保存在对象内部,无需额外分配控制块
  2. 更轻量
  3. intrusive_ptr 必须配合 add_ref() / release() 函数或接口

那么缺点也很明显:

  1. 要求被管理的对象必须要持有支持引用计数操作的接口,侵入了对象的定义
  2. 不能直接用于第三方或者不可修改的类型
  3. 生命周期管理耦合度高

侵入式智能指针的代表

  • boost::intrusive_ptr
  • 一些自定义的游戏引擎对象管理器
  • Qt的QSharedDataPointer(半侵入)

非侵入式智能指针

非侵入式智能指针的引用计数等都存储在智能指针内部的空值块中,而不是被管理的对象内部。

主要特点有

  1. 对象本身不需要任何修改,比如继承某个基类或者添加引用计数等
  2. 可以管理第三方类型,系统库类型或者原有代码,不会破坏封装
  3. 引用计数信息在控制块中,不在对象内

缺点

  1. 控制块会额外占用内存,但是 make_shared会优化(后面详细讲解)
  2. 多次用shared_ptr包装同一个裸指针会导致多份引用计数存在,导致重复析构

非侵入式智能指针的代表

  • std::shared_ptr
  • std::weak_ptr
  • std::uniuqe_ptr

三、unique_ptr

unique_ptr是C++11引入的,定义在<memory>头文件中。

核心特点

独占所有权

在同一时间只能有一个unique_ptr指向特定的对象,以此避免内存泄漏和悬垂指针的问题。也就是说,这个对象的所有权被unique_ptr拥有,禁止拷贝,但是允许资源所有权的转移,当unique_ptr销毁时,所指向的对象也会自动释放。

轻量化

unique_ptr大小与原始指针大小相同,一般是4字节或者8字节,无需额外的开销。

用法

  1. unique_ptr的基本用法,管理普通对象
#include <memory>
#include <iostream>

int main() {
	/// 1、可以管理动态分配的对象
	std::unique_ptr<int> ptr1(new int(10));
	std::unique_ptr<int> ptr2 = std::make_unique<int>(20);
	
	/// 2、可以管理动态分配的数组
	std::unique_ptr<int[]> arr_ptr(new int[3]{1, 2, 3});  /// 这里可以分别管理单个对象和数组的原理之后下一章节详细介绍
	return0;	
}
  1. 对管理的对象进行访问
struct MyClass {
	void print() {
		std::cout << "this is MyClass!" << std::endl;
	}
}

int main() {
	std::unique_ptr<MyClass> my_ptr = std::make_unique<MyClass>();
	/// 访问管理的对象
	my_ptr->print();
	/// 输出 this is MyClass!
	return 0;
}
  1. 资源所有权的转移
int main() {
	std::unique_ptr<int> ptr1 = std::make_unique<int>(11);
	/// 转移ptr1管理的资源,转移之后ptr1应该为空,不能再拥有对象
	std::unique_ptr<int> ptr2 = std::move(ptr1);
	
	if (ptr1 != nullptr) {
		std::cout << "ptr2 转移资源失败!!" << std::endl;
	}
	return 0;
}
  1. unique_ptr还提供了重置和释放当前管理对象的方法
int main() {
	std::unique_ptr<int> ptr1 = std::make_unique<int>(11);
	
	ptr1.reset(new int(100)); /// 	释放旧对象管理新对象
	
	int *release_ptr = ptr1.release();	/// 现在释放了拥有权,返回的是底层管理的裸指针 
	
	delete release_ptr;		/// 由于 unique_ptr已经被释放了,所以现在是裸指针,必须手动释放
	return 0;
}
  1. 还可以作为函数返回值和参数,但是需要使用移动语义,例如
std::unique_ptr<int> create_unique (int value) {
	return std::make_unique<int>(value);
}

void get(std::unique_ptr<int> ptr) {
	std::cout << "this is value:" << *ptr << std::endl;  /// 这里访问智能指针管理的内容
}

int main() {
	auto ptr = create_unique(11111);
	/// 使用移动语义传参
	get(std::move(ptr));  /// unique_ptr是独占所有权,所以必须使用资源转移的方式
	return 0;
}

源码剖析与实现

核心思想

资源独享,不允许拷贝和复制操作,必须删除对应的拷贝构造和赋值运算符,但是支持移动资源的方式,将一个智能指针的管理资源的所有权转移给另一个智能指针。
在交换资源时使用swap是一种最安全的方案,因为,swap可以自动交换内部指针,不会造成double free,即使类型不同,只要U能够转换为T就能兼容

默认删除器

unique_ptr使用了经典的策略模式+函数对象结合来使用,提供了两种默认的删除器,分别可以删除单个对象和数组,同时重载了operator()的方式进行delete

template <typename Tp>
struct default_deleter {
	constexpr default_deleter() noexcept = default;
	
	void operator()(Tp* prt) const {
		delete ptr;		/// 删除单个对象
	}
};

template <typename Tp>
struct default_deleter<Tp[]> {
	constexpr default_deleter() noexcept = default;
	
	void operator()(Tp* ptr) const {
		delete []ptr;
	}
};

这里为什么要重载 operator() 呢?

我觉得是期望 default_deleter具备可调用对象的性质,可以进行函数调用,称为自定义删除策略,重载operator()有以下几个好处:

  1. 可以作为类型传入模板
    因为模板参数必须是类型,不能是一个普通的函数,结构体是一个类型,所以可以传入一个结构体

    template <typename T, typename Deleter = default_delete<T>>
    class unique_ptr{};
    
  2. 支持状态记录,也就是在函数体内可以添加更多操作
    在重载函数体内部可以添加其他的操作,比如定义日志,添加成员变量等

    struct logging_deleter {
     void operator()(int* p) const {
     /// 在函数对象里面可以自定义日志,以及添加一些成员变量
         std::cout << "Deleting ptr\n";
         delete p;
     }
    };
    
  3. 支持数组类型
    通过特化 default_deleter<T[]>,可以专门对数组进行删除,而直接定义成delete_ptr(T* ptr) 的函数无法解决这个问题.

    default_deleter<int[]> d;
    int *arr = new int[2];
    d(arr); //  = delete []arr;
    

空指针构造

这里单独介绍一下空指针构造,在构造函数中传入std::nullptr_t,接受一个空指针作为参数,目的是显式构造一个空的unique_ptr,但是也需要重载 operator =来接收这个对象

/// 这里的参数是std::nullptr_t表明允许接收一个nullptr类型的参数,目的是可以显式构造一个空的unique_ptr
/// 这非常有用,比如作为默认初始化
explicit unique_ptr(std::nullptr_t) noexcept
    : ptr_(nullptr)
{}

/// 重载operator=接收nullptr_t
unique_ptr& operator=(std::nullptr_t ) noexcept
{
    reset();
    return *this;
}

内存安全提醒

在复现时一定要注意内存安全的问题,不能访问非法空间,时刻要注意判断指针是否为空等,所以一般可以使用静态断言(标准库的方法),我们可以参考。

static_assert(!is_void<_Tp>::value,
	"can't delete pointer to incomplete type"););

成员变量

成员变量主要是变量的指针以及删除器

using pointer = Tp *;
using delete_type = Dp;

pointer ptr_;
delete_type deleter_;

构造函数与析构

这里主要给出针对单个对象的管理方法,针对数组的方法可以在此基础之上,修改,原理是一致的。

constexpr unique_ptr() noexcept
                : ptr_(nullptr) {}

        explicit unique_ptr(pointer ptr) noexcept
                : ptr_(ptr) {}

        /// 这里的参数是std::nullptr_t表明允许接收一个nullptr类型的参数,目的是可以显式构造一个空的unique_ptr
        /// 这非常有用,比如作为默认初始化
        explicit unique_ptr(std::nullptr_t) noexcept
                : ptr_(nullptr) {}

        /// 拷贝构造和移动构造
        unique_ptr(pointer ptr, const delete_type &deleter) noexcept
                : ptr_(ptr), deleter_(deleter) {}

        unique_ptr(pointer ptr, delete_type &&deleter) noexcept
                : ptr_(ptr), deleter_(my::move(deleter)) {}

        /// unique_ptr的核心是不允许通过unique_ptr进行拷贝和赋值
        unique_ptr(const unique_ptr &other) = delete;

        unique_ptr &operator=(const unique_ptr &other) = delete;

        /// 模板移动构造函数和模板移动赋值运算符
        /// 允许从其它类型的 unique_ptr<U> 构造或赋值,只要 U* 可以转换为 T*
        template<typename U>
        unique_ptr(unique_ptr<U> &&other) noexcept
                : ptr_(nullptr) {
            static_assert(std::is_convertible<U *, Tp *>::value,
                          "U* must be convertible to Tp*");

            /// 使用swap是一种安全移动资源的方法,可以自动交换内部指针;原指针置空,不会double free,
            /// 即使类型不同(只要U*能够转换为T*)就能兼容
            other.swap(*this);
        }

        template<typename U>
        unique_ptr &operator=(unique_ptr<U> &&other) noexcept {
            other.swap(*this);
            return *this;
        }

        /// 重载operator=接收nullptr_t
        unique_ptr &operator=(std::nullptr_t) noexcept {
            reset();
            return *this;
        }

        ~unique_ptr() {
            reset();
        }

常见的接口方法

重置与释放方法

reset()重置当前管理的资源,需要首先释放原来管理的资源,然后持有新的资源

void reset(pointer ptr = pointer()) noexcept {
            /// 重置指针,删除当前管理的对象
            /// 需要做自删除判断
            if (ptr_ != ptr) {
                pointer temp = ptr_;
                ptr_ = ptr;
                if (temp) {
                	/// 析构原始的资源
                    deleter_(temp);
                }
            }
        }

release()释放对所管理对象的所有权,并且返回原始裸指针,但不会调用删除器析构对象本身。

 pointer release() noexcept {
            /// 释放控制权但不释放资源,这里指的是将Ptr的所有权交出,不会调用删除器,返回原始指针,由程序自己管理资源
            pointer temp = ptr_;            /// 拷贝当前持有的指针到 temp
            ptr_ = nullptr;                 /// 将内部指针ptr_设置为nullptr(防止double free)
            return temp;                    /// 返回原始指针
        }

获取所管理对象的裸指针

给外部用户提供了获取底层管理的原始裸指针的接口

pointer get() const noexcept {
	retrun ptr_;		/// ptr_是管理对象的裸指针
}

注意

  1. 我们在使用时还是需要避免长期持有get()返回的指针,因为如果unique_ptr释放了对象,这个指针就会变成野指针;
  2. 也不能使用get()的结果初始化另外一个智能指针,否则就会导致多个智能指针同时管理同一个对象,引发 double free

获取底层的删除器

/// 获取删除器
delete_type getdeleter() noexcept {
      return deleter_;
}

const delete_type getdeleter() const noexcept {
     return deleter_;
}

重载相关运算符

主要是 -> * ()

 explicit operator bool() const noexcept {
            return ptr_ != nullptr;
}

pointer operator->() const noexcept {
        return get();
}

/// 解引用
Tp &operator*() const noexcept {
       return *ptr_;
}

交换资源

使用swap交换资源资源

void swap(unique_ptr& other) noexcept {
	/// 交换所有成员变量
	std::swap(ptr_, other.ptr_);
    std::swap(deleter_, other.deleter_);	
}

针对数组对象方式

数组实现方式类似,大家可以自己实现

代码测试

代码测试有一些专业的代码,但是我目前还没有实际掌握,所以这里仅仅测试一些基础的功能,代码如下:

#include "Myunique_ptr.h"
#include <iostream>

/// 基本使用和析构
struct Foo {
    Foo(int x) : x(x) {}
    ~Foo() { std::cout << "Foo " << x << " destroyed\n"; }
    int x;
};

void test_basic() {
    my_unique_ptr::unique_ptr<Foo> ptr(new Foo(42));
    std::cout << ptr->x << std::endl;
    std::cout << "test_basic success!!"<< std::endl;

}

/// release()转交资源
void test_release() {
    my_unique_ptr::unique_ptr<Foo> ptr(new Foo(10));
    Foo* raw = ptr.release();
    std::cout << raw->x << std::endl;
    delete raw; /// 手动释放
    std::cout << "test_release success!!"<< std::endl;
}

/// reset() 与自删除保
void test_reset()
{
    my_unique_ptr::unique_ptr<Foo> ptr(new Foo(20));
    Foo* same = ptr.get();          /// 将ptr中的指针获取赋给same
    ptr.reset(same);            /// 自己删除自己测试,正常情况不应该崩溃
    std::cout << "test_reset success!!"<< std::endl;
}

/// 测试数组的情况
void test_array()
{
    my_unique_ptr::unique_ptr<int[]> arr(new int[5]{1, 2, 3, 4, 5});
    for (size_t i = 0; i < 5; i++) {
        std::cout << arr[i] << " ";
    }
    std::cout << std::endl;
    std::cout << "test_array success!!"<< std::endl;
}
int main()
{
    test_basic(); // 42  Foo 42 destroyed
    test_release();
    test_reset();
    test_array();

    return 0;
}

这里的测试结果

        42
        test_basic success!!
        Foo 42 destroyed
        10
        Foo 10 destroyed
        test_release success!!
        test_reset success!!
        Foo 20 destroyed
        1 2 3 4 5
        test_array success!!

四、shared_ptr与weak_ptr

shared_ptr

shared_ptr使用了引用计数,每一个shared_ptr的拷贝都指向相同的内存,每次拷贝都是触发引用计数+1,每次生命周期结束析构的时候引用计数减一,最后一个shared_ptr析构的时候,内存才会释放。

自定义删除器

shared_ptr也可以自定义删除器,在引用计数为0时,自动调用删除器释放对象的内存,一般可以使用Lambda表达式实现。

std::shared_ptr<int> ptr(new int, [](int* p) {delete p;});

特别注意

  1. 不能使用多个shared_ptr指向同一个裸指针,可能导致 double free
  2. 不能直接返回this指针,直接返回this是裸指针,可能会导致对冲析构,需要使用shared_from_this(),其实这里就是返回weak_ptr指针
  3. 尽可能使用make_shared,具体原因后面会分析
  4. 不要delete 通过get()获取的裸指针
  5. 不是new出来的空间要自定义删除器
  6. 要避免循环引用问题

插播一个循环引用

class B;
class A {
	std::shared_ptr<B> a_ptr;
};

class B {
	std::shared_ptr<A> b_ptr;		/// 这样会导致循环引用
	/// 改成 std::weak_ptr<A> b_ptr; 解决这个问题
}

int main() {
	std::shared_ptr<A> a = std::make_shared<A>();
	std::shared_ptr<B> b = std::make_shared<B>();
	a->a_ptr = b;
	b->b_ptr = a;
	
	return 0;
	
}

weak_ptr

主要作用是用来监视shared_ptr的生命周期是否还存在,即是否过期,不管理shared_ptr的内部指针,它的拷贝析构都不会影响引用计数。

主要作用

  1. 可以解决循环引用问题
  2. 可以使用shared_from_this()返回this指针

核心操作

weak_ptrshared_ptr构造。对于其中的lock() expired()的使用都需要检查对象是否存活

适用场景

  • 观察者模式、缓存以及需要避免循环引用时
  • 但是不能直接访问对象,需要转换为shared_ptr才能访问对象的资源。

核心机制

当多个shared_ptr可共享同一对象的所有权时,通过“引用计数”追踪对象生命周期

  • 引用计数在构造时 +1,析构时-1,为0释放所管理的对象
  • 控制块(Control Block):存储引用计数(use_count)、弱引用计数(weak_count)、自定义删除器等

结合源码分析shared_ptr与weakptr的底层逻辑

总体架构

shared_ptr<T>----------------->control block <-----------------weak_ptr
		                     |
				     |
				     ▼
				所管理的对象指针(T*)

control block是负责主要管理对象的,主要负责管理的内容有:

  1. 指向实际对象的裸指针T*
  2. 引用计数 use_count
  3. 弱引用计数weak_count,包括weak_ptrshared_ptr本身对控制块的引用
  4. 删除器,主要有delete free以及自定义的删除器等

控制块基类 sp_count_base

主要成员变量

std::atomic<int32_t> use_count_;
std::atomic<int32_t> weak_count_;

利用原子变量保证线程安全。

提供的操作接口

  1. 构造和析构
    默认构造和析构,构造控制块基类时必须将强引用计数和弱引用计数都初始化为1,即一个shared_ptr初始化时必须要有一个weak_ptr,防止其提前销毁。
/// 默认构造函数,初始化引用计数,每当有一个shared_ptr时也必须要有一个weak_ptr的引用计数,防止其提前销毁
sp_count_based() noexcept
                : use_count_(1), weak_count_(1) {}

/// 作为基类,析构函数应该是虚函数
virtual ~sp_count_based() noexcept {}
  1. 释放对象与释放控制块自身
    dispose()destroy()分别表示释放控制块管理的对象以及释放内存控制块本身
    shared_ptruse_count减为0但weak_ptr不为0时,释放管理的对象,等weak_count == 0时释放控制块本身。
/// dispose、destroy都是虚函数,由子类实现释放的逻辑
/// 当use_count_ == 0时调用,释放被管理的对象
virtual void dispose() noexcept = 0;

/// 当weak_count_==0时调用,销毁整个控制块对象
virtual void destroy() noexcept {
        delete this;
}

  1. 引用计数相关接口
    提供了对use_count_weak_count对应的操作方法,在源码中很容易就可以看出其逻辑
/// 返回当前的引用计数
        int32_t use_count() const noexcept {
            return use_count_;
        }

        int32_t weak_count() const noexcept {
            return weak_count_;
        }

        //// 管理弱引用计数器
        void weak_add_ref() noexcept {
            /// 弱引用计数++
            ++weak_count_;
        }

        /// 若指针的释放
        void weak_release() noexcept {
            /// 这里释放的逻辑就是如果引用计数weak_count_释放到0了,说明应该释放掉了
            if (--weak_count_ == 0) {
                destroy();
            }
        }
        
        /// 管理use_count_引用计数,-1以及释放
        void add_ref_copy() noexcept {
            ++use_count_;
        }

        void release() noexcept {
            if (--use_count_ == 0) {
                dispose();      /// 这里销毁管理的资源
                weak_release(); /// 销毁控制块本身
            }
        }
        
          /// 控制块基类中的 add_ref_lock 实现
    virtual bool add_ref_lock()
    {
        if (use_count_ == 0) {
            return false;
        }
        ++use_count_;
        return true;
    }
        
        

add_ref_lock():这个接口判断当前的shared_ptr对象是否还存在,如果引用计数已经为0了,说明对象已经不在了,返回false,如果还存在 引用计数+1,这里主要用于weak_ptr进行lock提升为shared_ptr的情景。

  1. 不允许 拷贝构造和赋值
 /// 控制块是唯一对应一份资源的,禁止拷贝构造与赋值
sp_count_based(const sp_count_based &) = delete;

sp_count_based &operator=(const sp_count_based &) = delete;

自定义删除器 sp_count_deleter

自定义删除器在基类的基础上实现,并且对 dispose()destroy()进行重写,其主要的成员变量和成员函数也是基于引用计数展开,代码如下:

template<typename Ptr, typename Deleter>
    class sp_count_deleter final : public sp_count_based {
    public:
        sp_count_deleter(Ptr ptr, Deleter deleter) noexcept
                : ptr_(ptr), deleter_(deleter) {}

        /// 同样删除器只允许一个对象持有,删除拷贝构造和赋值运算
        sp_count_deleter(const sp_count_deleter &) = delete;

        sp_count_deleter &operator=(const sp_count_deleter &) = delete;

        ~sp_count_deleter() noexcept {}

        /// 对dispose、destroy、ptr函数进行重写
        void *ptr() override {
            return ptr_;        /// 返回这个删除器管理的原始指针
        }

        /// const版本
        const void *ptr() const override {
            return ptr_;
        }

        /// dispose、destroy都可以继续被继承,所以写成虚函数
        virtual void dispose() noexcept override {
            if (ptr_) {
                deleter_(ptr_);     /// 调用删除器删除管理的对象
            }
        }

        /// 只有 sp_count_ptr_inplace是特例
        virtual void destroy() noexcept override {
            /// 销毁删除器对象
            delete this;
//            this->~sp_count_deleter();
        }

    private:
        Ptr ptr_;       /// 需要控制的指针,从外部指定模板参数
        Deleter deleter_;   /// 删除器
    };

内存缓冲区结构体

预留对齐原始内存缓冲区,用于在不重复堆分配的情况下,同时容纳 shared_ptr 的控制块 + 被管理对象 T。

结构定义与核心作用

template<typename _Tp>
struct __aligned_buffer

这是一个模板结构,表示专门为类型_Tp预留的一块 未构造但内存对齐的原始空间,用于在其中构造类型_Tp 的对象。

成员变量 _M_storage:底层存储空间

typename std::aligned_storage<sizeof (_Tp), alignof(_Tp)>::type _M_storage;
/// 例如
std::aligned_storage<16, 8>::type  // 分配16字节,按8字节对齐
  • sizeof(_Tp):确保这块内存大小足够容纳 _Tp
  • alignof(_Tp):确保这块内存满足 _Tp的对齐要求
    这种类型 不能直接用作_Tp类型的变量,因为它只是原始存储,未调用构造函数。必须手动用 placement new方法去构造它:

placement new

默认构造函数:什么都不做,不构造 _Tp
nullptr_t构造函数:是一个标记构造函数,用于跳过无意义的初始化(通常 make_shared用它)

__aligned_buffer() = default;
__aligned_buffer(std::nullptr_t) {}

主要的实现细节

_M_addr() / _M_addr() const_M_ptr() / _M_ptr() const
_M_addr()/ _M_addr() const返回原始裸指针,用于 placement new
_M_ptr() / _M_ptr() const返回类型安全的_Tp*指针;调用 placement new 构造后,可以用这个指针访问 _Tp

void* _M_addr() { return static_cast<void*>(&this->_M_storage); }

_Tp* _M_ptr() { return static_cast<_Tp*>(_M_addr()); }

实现场景

常用做与make_shared结合,其流程图如下

std::make_shared<T>(args...)
│
├── 分配一整块内存:控制块 + __aligned_buffer<T>
│
├── 使用 placement new 在 buffer 中构造 T 对象
│
├── 构造 shared_ptr<T>(控制块, buffer._M_ptr())
│
└── 控制块析构时:
     ├── 调用 T::~T()
     └── delete 控制块释放整块内存

sp_count_ptr控制块类

用于托管一个new创建的原始指针

  • shared_ptr<T>(new T)构造; new T 创建对象,原始指针传入sp_count_ptr<T>;
  • shared_ptr 管理 sp_count_ptr<T>*,间接管理T*;
  • 当所有 shared_ptr 销毁时,调用dispose() 释放T*;
  • 当所有shared_ptr和 weak_ptr 都销毁时,调用 destroy() 释放控制块本身

in-place控制块(placement control block)

make_shared<T>()时使用的控制块类,它与对象_Tp 一起构造在同一块连续堆内存中,避免多次mallocnew
总的来说就是在分配的那一块原始内存对齐的空间上,构造对象。
几个要点如下:

生命周期

  1. virtual void dispose() noexcept override:shared_ptr引用计数归零时触发的析构操作
std::allocator<_Tp> alloc;
std::allocator_traits<std::allocator<_Tp>>::destroy(alloc, static_cast<_Tp*>(ptr()));

显式调用 _Tp 的析构函数,释放逻辑资源。但是在这个时候:

  • 控制块还存在,即weak_count > 0;
  • 这里不调用 delete,因为 make_shared内存是一整块分配的
  1. virtual void destroy() noexcept override:这是 weak_ptr引用计数归零后触发的控制块销毁操作。这里需要调用this->~sp_count_ptr_inplace();,为什么这里特殊?不调用delete this?原因在于sp_count_ptr_inplace对象本身是通过 ::operator new(sizeof(sp_count_ptr_inplace + sizeof(T)))一次性分配的大内存块中的一部分,不可以单独delete。所以只能调用析构函数,让整块内存稍后统一释放(通常由 shared_ptr的自定义 operator delete完成释放)

整体的内存布局

整体内存在堆上申请,避免了两次分配(传统 shared_ptr 需要为控制块和对象各分配一次)

┌──────────────────────────────┐
│ sp_count_ptr_inplace<T>      │  ← 控制块头部(引用计数、vtable等)
├──────────────────────────────┤
│ __aligned_buffer<T>          │  ← T 对象的原始存储空间
├──────────────────────────────┤
│ T 实例(placement new)      │  ← 实际构造 T(args...)
└──────────────────────────────┘

完整的调用流程

  1. 调用 operator new(sizeof(sp_count_ptr_inplace<T>))
  2. 在这块内存上构造 sp_count_ptr_inplace<T>(args...)
  3. 使用 placement newstorage_中构造 T(args...)
  4. 返回 shared_ptr<T>,其管理的控制块是 sp_count_ptr_inplace

shared_ptr整体流程

shared_ptr<T>
 ├── ptr_: T*
 └── refcount_: shared_count
       └── pi_: sp_count_based* 控制块
                 ├── use_count_: int    // shared_ptr 计数
                 ├── weak_count_: int   // weak_ptr 计数
                 ├── virtual dispose()  // 析构对象
                 └── virtual destroy()  // 销毁控制块

控制块可以是以下三者之一

  • sp_count_ptr<T>(普通 new T)—>使用普通的new T来创建对象时所生成的控制块,像shared_ptr<T>(new T)这样的方式就会生成这种控制块。
  • sp_count_deleter<T,D>(自定义删除器)–>当采用自定义删除器时,就会生成这种控制块,例如shared_ptr<T>(new T, custom_deleter)
  • sp_count_ptr_inplace<T>(用于make_shared)->在使用make_shared<T>()时,会生成该控制块。
    以上三者的差异
  1. sp_count_ptr<T>控制块里存放一个指向对象的指针,并且会在析构时调用delete ptr_;
  2. sp_count_deleter<T,D>控制块除了保存对象指针,还保存着一个删除器,在析构时会调用这个删除器
  3. sp_count_ptr_inplace<T>控制块内部直接保存对象实例,而非指针,析构时会直接调用对象的析构函数

对象生命周期的完整调用

(1)、创建 shared_ptr<T>(new T)与析构时的调用链

shared_ptr<T> p(new T);

/// 构造流程
-> shared_ptr(T*) → shared_count(T*) 
   → new sp_count_ptr<T>(T*) → 构造控制块,use_count=1, weak_count=1

析构时

p.~shared_ptr()
 -> shared_count::~shared_count()
     -> pi_->release()
         -> --use_count == 0 → pi_->dispose()(delete ptr_)
         -> --weak_count == 0 → pi_->destroy()(delete this)

(2)、创建 make_shared<T>(...)与析构时的调用链

auto p = make_shared<T>(args...);

/// 调用链
-> make_shared()
   -> shared_ptr<T>(sp_make_shared_tag, ...)
      -> shared_count(sp_make_shared_tag, T*, args...)
         -> new sp_count_ptr_inplace<T>(args...) 
            → 在内联 buffer 中 placement new 构造 T
            → use_count = 1, weak_count = 1
            
            
/// 以下是析构的流程
p.~shared_ptr()
  -> shared_count::~shared_count()
     -> pi_->release()
         → use_count == 0 → pi_->dispose() 手动析构 T(但不释放内存)
         → weak_count == 0 → pi_->destroy()(手动调用 ~sp_count_ptr_inplace)

(3)、创建 weak_ptr<T>(shared_ptr<T> const&)与析构时的调用链

shared_ptr<T> p = ...;
weak_ptr<T> wp(p);

/// 调用链
-> weak_ptr(p)
   → weak_count(shared_count&)
      → pi_->weak_add_ref() /// weak_count++
      
/// 以下是销毁流程
wp.~weak_ptr()
 → weak_count::~weak_count()
     → pi_->weak_release()
         → weak_count--;如果 == 0 且 use_count == 0 → pi_->destroy()


shared_ptr特化版本shared_ptr<T[]>

1、与shared_ptr<T>的区别

shared_ptr<T[]>是管理一个数组,shared_ptr<T>管理单个对象,那么释放的时候也需要针对数组进行释放而不是单个对象,跟unique_ptr一样,提供了两种不同的默认删除器

/**
 * C++标准库专门使用operator()来封装删除行为
 * 原因:希望default_deleter可以像一个可调用对象(仿函数),可以像函数调用一样使用
 * */
/// 提供2个默认删除器,一个可以针对单个对象进行删除,一个可以针对数组进行删除
/// 策略模式 + 函数对象(函数仿函数)
template<typename Tp>
struct default_deleter{
    constexpr default_deleter() noexcept = default;

    void operator()(Tp* ptr) const
    {
        delete ptr;
    }
};

/// 针对数组进行删除
template<typename Tp>
struct default_deleter<Tp[]>{
    constexpr default_deleter() noexcept = default;

    void operator()(Tp* ptr) const
    {
        delete []ptr;
    }
};

在调用时:

std::shared_ptr<int[]> arr(new int[10]);  // 正确!
arr[0] = 42;

/// 实际上调用的是
shared_ptr<int[]>(new int[10], default_delete<int[]>());

当引用计数为0时:

// sp_count_deleter<T*, default_delete<T[]>>
-> dispose() {
    deleter_(ptr_);
}

智能指针的对比

智能指针类型 所有权模型 引用计数 拷贝/转移特性 典型适用场景 性能开销
unique_ptr 独占所有权 不可拷贝,可转移 单个所有者管理资源、函数返回值 接近原始指针
shared_ptr 共享所有权 可拷贝(引用计数+1) 多所有者共享资源、容器存储对象 引用计数增减开销
weak_ptr 无所有权(弱引用) 不影响 不直接管理资源 打破循环引用、临时访问共享资源 轻量(依赖shared_ptr

发表回复

Your email address will not be published. Required fields are marked *.

*
*

近期评论

您尚未收到任何评论。

conviction

要想看到璀璨的日出时刻,就要有跋山涉水的决心和坚毅