【c++】【智能指针】shared_ptr底层实现
【c++】【智能指针】shared_ptr底层实现
智能指针之前已经写过了,但是考虑到不够深入,应该再分篇写写。
1 shared_ptr
1.1 shared_ptr 是什么
std::shared_ptr是一个类模板,它的对象行为像指针,但是它还能记录有多少个对象共享它管理的内存对象。
多个std::shared_ptr可以共享同一个对象。
当最后一个std::shared_ptr被销毁时,它会自动释放它所指向的对象。
1.2 shared_ptr创建和销毁
可以通过make_shared<>函数来创建。
可以通过拷贝或赋值另一个shared_ptr来创建。
eg:sp1和sp2指向同一个对象,内存对象的引用计数为2。当sp1被销毁时,引用计数减为1,sp2仍然指向该对象。当sp2被销毁时,引用计数减为0,内存对象被销毁。
1.3 shared_ptr的底层原理
element_type* _M_ptr; // 所管理对象的地址.
__shared_count<_Lp> _M_refcount; // 引用计数块地址.
std::shared_ptr在内部其只有两个指针成员:
- 一个指针是所管理的数据的地址
- 一个指针是控制块的地址
包括引用计数
weak_ptr计数
删除器(Deleter)
分配器(Allocator)
因为不同shared_ptr指针需要共享相同的内存对象,因此引用计数的存储是在 堆
上的。
而unique_ptr只有一个指针成员,指向所管理的数据的地址。因此一个shared_ptr对象的大小是它大小的两
倍。
eg:vs2022-64下:
int main()
{
std::cout << sizeof(std::shared_ptr<int>) << std::endl; // 8
std::cout << sizeof(std::unique_ptr<int>) << std::endl; // 4
}
1.4 std::shared_ptr的简单实现
我们通过下面这个简单的类来模拟std::shared_ptr<>的实现,来理解引用计数的实现原理。
这里我们为了简单,只实现了
- shared_ptr的拷贝构造函数、析构函数和赋值运算符函数,引用计数只是简单地用了一个int类型的内存空间
- 省略了weak_ptr的计数、删除器和分配器,不考虑多线程的情况。
- 当我们销毁一个shared_ptr时,引用计数减1。当引用计数减为0时,我们删除指向实际数据的指针和指向引用计数的指针。
- 当我们拷贝一个shared_ptr时,引用计数加1。
- 当我们赋值一个shared_ptr时,我们首先递减左侧运算对象的引用计数。如果引用计数变为0,我们就释放左侧运算对象分配的内存以及引用计数的内存。然后拷贝右侧运算对象的数据指针和引用计数指针,最后递增引用计数。
template<typename T>
class shared_ptr {
public:
// 构造函数
// 初始化智能指针,传入一个裸指针(默认为 nullptr)
shared_ptr(T* ptr = nullptr) : m_ptr(ptr), m_refCount(new int(1)) {}
// 拷贝构造函数
// 通过另一个 shared_ptr 构造,增加引用计数
shared_ptr(const shared_ptr& other) : m_ptr(other.m_ptr), m_refCount(other.m_refCount) {
// 增加引用计数
(*m_refCount)++;
}
// 析构函数
~shared_ptr() {
// 减少引用计数
(*m_refCount)--;
// 如果引用计数为 0,释放内存
if (*m_refCount == 0) {
delete m_ptr;
delete m_refCount;
}
}
// 重载赋值运算符
shared_ptr& operator=(const shared_ptr& other) {
// 检查自我赋值
if (this != &other) {
// 减少旧对象的引用计数
(*m_refCount)--;
// 如果引用计数为 0,释放内存
if (*m_refCount == 0) {
delete m_ptr;
delete m_refCount;
}
// 复制新对象的数据和引用计数指针,并增加引用计数
m_ptr = other.m_ptr;
m_refCount = other.m_refCount;
// 增加引用计数
(*m_refCount)++;
}
return *this;
}
private:
T* m_ptr; // 指向实际数据的指针
int* m_refCount; // 引用计数
};
ps:多线程时 引用计数的++操作需要是原子性的。考虑使用std::atomic
- 是 C++11 引入的模板类,位于头文件 中,主要用于在多线程环境下实现原子操作,从而避免数据竞争(data race),保证线程安全。
1.5 什么时候用 std::shared_ptr<T>
?
std::shared_ptr<T>
主要用于以下场景:
1. 资源创建昂贵、比较耗时的场景
- 创建对象代价很高(例如文件、网络、数据库等),不希望频繁创建和销毁。
- 通过共享指针来管理对象生命周期,避免频繁创建和释放导致的性能损耗。
示例:管理数据库连接
#include <iostream>
#include <memory>
class Database {
public:
Database() { std::cout << "Connecting to Database\n"; }
~Database() { std::cout << "Closing Database Connection\n"; }
};
void useDatabase(std::shared_ptr<Database> db) {
std::cout << "Using Database\n";
}
int main() {
auto db = std::make_shared<Database>(); // 资源创建昂贵,使用共享指针管理
useDatabase(db); // 共享所有权
}
在这个例子中,Database连接创建和释放都很昂贵,使用 shared_ptr 可以在多个对象间安全地共享连接(并非多线程),等所有使用者都释放之后才会关闭连接。
- std::shared_ptr` 的引用计数是线程安全的(即对引用计数的增加和减少是原子操作,不会导致竞争条件)
- 但对实际管理的对象的操作是非线程安全的。
ps: 之前的文章有提到
在示例中,以下部分是线程安全的:
- 引用计数的增加和减少
- 判断对象是否需要释放
- 只读访问是线程安全的
需要加锁的部分:
- 对实际对象(
Database
)的数据修改需要加锁。
如果多个线程同时修改Database
对象的内容,可能会发生数据竞争,导致未定义行为。因此,需要在访问或修改对象时加锁。
2. 需要共享资源的所有权
- 一个对象的生命周期可能同时涉及多个对象,但不清楚谁会最终释放这个对象。
std::shared_ptr
使用引用计数,确保在最后一个shared_ptr
离开作用域时才释放资源。
示例:对象被多个对象共享
#include <iostream>
#include <memory>
class A {
public:
A() { std::cout << "A constructed\n"; }
~A() { std::cout << "A destroyed\n"; }
};
int main() {
std::shared_ptr<A> sp1 = std::make_shared<A>();
std::shared_ptr<A> sp2 = sp1; // 引用计数 +1
std::shared_ptr<A> sp3 = sp2; // 引用计数 +1
std::cout << "Use count: " << sp1.use_count() << "\n"; // 输出 3
sp2.reset(); // 引用计数 -1
std::cout << "Use count after sp2 reset: " << sp1.use_count() << "\n"; // 输出 2
sp1.reset(); // 引用计数 -1
std::cout << "Use count after sp1 reset: " << sp3.use_count() << "\n"; // 输出 1
sp3.reset(); // 最终释放对象
}
sp1
,sp2
,sp3
共享对A
对象的所有权。- 只有最后一个
shared_ptr
释放时,才会析构A
对象。
不适用 shared_ptr的情况
❌ 1. 存在明显的所有者 → 使用 unique_ptr
更合适。
❌ 2. 不需要共享所有权 → 使用裸指针或 unique_ptr
。
❌ 3. 循环引用问题 → 使用 weak_ptr
解决。
部分转自:https://zhuanlan.zhihu.com/p/672745555?utm_source=chatgpt.com