深入理解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 Initialization, RAII)”,将资源与对象的生命周期绑定,能够自动管理内存的生命周期,减少人为操作导致的失误。
二、智能指针基础介绍
核心原理
RAII
RAII表示资源获取与对象的构造进行绑定,在对象的生命周期结束时,释放对应的资源,通过类封装原始指针,在析构函数中执行释放操作实现RAII的行为。
所有权模型
智能指针的核心是通过“所有权”机制对对象进行管理,不同的智能指针的所有权的策略不同,常见的策略有独占、共享等。
智能指针的分类
智能指针可以分为侵入式智能指针与非侵入式智能指针,核心不同在于控制块(`control block
) 由谁“提供”。
侵入式智能指针
对象自己包含了引用计数变量,智能指针只负责调用引用计数的加减,不持有控制块。
主要特点有
- 引用计数直接保存在对象内部,无需额外分配控制块
- 更轻量
intrusive_ptr
必须配合add_ref()
/release()
函数或接口
那么缺点也很明显:
- 要求被管理的对象必须要持有支持引用计数操作的接口,侵入了对象的定义
- 不能直接用于第三方或者不可修改的类型
- 生命周期管理耦合度高
侵入式智能指针的代表
boost::intrusive_ptr
- 一些自定义的游戏引擎对象管理器
- Qt的
QSharedDataPointer
(半侵入)
非侵入式智能指针
非侵入式智能指针的引用计数等都存储在智能指针内部的空值块中,而不是被管理的对象内部。
主要特点有
- 对象本身不需要任何修改,比如继承某个基类或者添加引用计数等
- 可以管理第三方类型,系统库类型或者原有代码,不会破坏封装
- 引用计数信息在控制块中,不在对象内
缺点
- 控制块会额外占用内存,但是
make_shared
会优化(后面详细讲解) - 多次用
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字节,无需额外的开销。
用法
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;
}
- 对管理的对象进行访问
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;
}
- 资源所有权的转移
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;
}
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;
}
- 还可以作为函数返回值和参数,但是需要使用移动语义,例如
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()有以下几个好处:
-
可以作为类型传入模板
因为模板参数必须是类型,不能是一个普通的函数,结构体是一个类型,所以可以传入一个结构体template <typename T, typename Deleter = default_delete<T>> class unique_ptr{};
-
支持状态记录,也就是在函数体内可以添加更多操作
在重载函数体内部可以添加其他的操作,比如定义日志,添加成员变量等struct logging_deleter { void operator()(int* p) const { /// 在函数对象里面可以自定义日志,以及添加一些成员变量 std::cout << "Deleting ptr\n"; delete p; } };
-
支持数组类型
通过特化 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_是管理对象的裸指针
}
注意:
- 我们在使用时还是需要避免长期持有
get()
返回的指针,因为如果unique_ptr
释放了对象,这个指针就会变成野指针; - 也不能使用
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;});
特别注意
- 不能使用多个
shared_ptr
指向同一个裸指针,可能导致 double free - 不能直接返回
this
指针,直接返回this
是裸指针,可能会导致对冲析构,需要使用shared_from_this()
,其实这里就是返回weak_ptr
指针 - 尽可能使用
make_shared
,具体原因后面会分析 - 不要delete 通过get()获取的裸指针
- 不是new出来的空间要自定义删除器
- 要避免循环引用问题
插播一个循环引用
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
的内部指针,它的拷贝析构都不会影响引用计数。
主要作用
- 可以解决循环引用问题
- 可以使用
shared_from_this()
返回this
指针
核心操作
weak_ptr
从shared_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
是负责主要管理对象的,主要负责管理的内容有:
- 指向实际对象的裸指针
T*
- 引用计数
use_count
- 弱引用计数
weak_count
,包括weak_ptr
和shared_ptr
本身对控制块的引用 - 删除器,主要有delete free以及自定义的删除器等
控制块基类 sp_count_base
主要成员变量
std::atomic<int32_t> use_count_;
std::atomic<int32_t> weak_count_;
利用原子变量保证线程安全。
提供的操作接口
- 构造和析构
默认构造和析构,构造控制块基类时必须将强引用计数和弱引用计数都初始化为1,即一个shared_ptr
初始化时必须要有一个weak_ptr
,防止其提前销毁。
/// 默认构造函数,初始化引用计数,每当有一个shared_ptr时也必须要有一个weak_ptr的引用计数,防止其提前销毁
sp_count_based() noexcept
: use_count_(1), weak_count_(1) {}
/// 作为基类,析构函数应该是虚函数
virtual ~sp_count_based() noexcept {}
- 释放对象与释放控制块自身
dispose()
与destroy()
分别表示释放控制块管理的对象以及释放内存控制块本身
当shared_ptr
的use_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;
}
- 引用计数相关接口
提供了对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
的情景。
- 不允许 拷贝构造和赋值
/// 控制块是唯一对应一份资源的,禁止拷贝构造与赋值
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
一起构造在同一块连续堆内存中,避免多次malloc
和 new
总的来说就是在分配的那一块原始内存对齐的空间上,构造对象。
几个要点如下:
生命周期
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
内存是一整块分配的
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...)
└──────────────────────────────┘
完整的调用流程
- 调用
operator new(sizeof(sp_count_ptr_inplace<T>))
- 在这块内存上构造
sp_count_ptr_inplace<T>(args...)
- 使用
placement new
在storage_
中构造T(args...)
- 返回
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>()
时,会生成该控制块。
以上三者的差异:
sp_count_ptr<T>
控制块里存放一个指向对象的指针,并且会在析构时调用delete ptr_
;sp_count_deleter<T,D>
控制块除了保存对象指针,还保存着一个删除器,在析构时会调用这个删除器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 ) |