基础介绍
c++11版本引入了智能指针,其中std::shared_ptr<T>是其中一种,这是一个模板类,它的类的简化实现如下所示:
template<typename T>
class shared_ptr {
private:
T* ptr; // 指向管理的对象
int* count; // 引用计数
// 释放资源的辅助函数
void release() {
if (count) {
(*count)--;
if (*count == 0) {
delete ptr;
delete count;
}
}
}
public:
// 默认构造函数
shared_ptr() : ptr(nullptr), count(nullptr) {}
// 通过原始指针构造
explicit shared_ptr(T* p) : ptr(p) {
count = new int(1);
}
// 拷贝构造函数
shared_ptr(const shared_ptr& other) : ptr(other.ptr), count(other.count) {
if (count) {
(*count)++;
}
}
// 移动构造函数
shared_ptr(shared_ptr&& other) noexcept : ptr(other.ptr), count(other.count) {
other.ptr = nullptr;
other.count = nullptr;
}
// 析构函数
~shared_ptr() {
release();
}
// 拷贝赋值运算符
shared_ptr& operator=(const shared_ptr& other) {
if (this != &other) {
// 释放当前资源
release();
// 复制新资源
ptr = other.ptr;
count = other.count;
if (count) {
(*count)++;
}
}
return *this;
}
// 移动赋值运算符
shared_ptr& operator=(shared_ptr&& other) noexcept {
if (this != &other) {
// 释放当前资源
release();
// 移动资源
ptr = other.ptr;
count = other.count;
other.ptr = nullptr;
other.count = nullptr;
}
return *this;
}
// 重载操作符
T& operator*() const { return *ptr; }
T* operator->() const { return ptr; }
// 获取原始指针
T* get() const { return ptr; }
// 获取引用计数
int use_count() const { return count ? *count : 0; }
// 检查是否为空
bool operator bool() const { return ptr != nullptr; }
// 重置指针
void reset(T* p = nullptr) {
release();
ptr = p;
if (p) {
count = new int(1);
} else {
count = nullptr;
}
}
};
依赖的头文件:#include <memory>
std::shared_ptr<T>是一个共享智能指针,多个共享指针对象可以共享同一片数,之所以称为智能指针是因为它可以自动实现资源的释放管理(通过数据控制块中的引用计数来实现),这与new/delete不同,new申请的数据块需要手动delete删除。std::shared_ptr<T>对象的内存结构如下:
a (shared_ptr对象)
|--> ptr指针 --------→ [指向对应实际的数据块]
|--> control block指针 → [Heap: 数据控制块]
|- use_count = 1
|- weak_count = 1
|- deleter
|- allocator
从上面的内存结构可以看出,shared_ptr对象包含两个成员,且都是指针:
- ptr指针:指向实际的数据块
- 数据库控制块指针:指向数据控制块,在这个控制块中包含了引用数,弱引用数等信息
思考:若两个shard_ptr对象做复制操作,内存会发生什么?
请看下面的代码:
class MyClass
{
private:
int a;
std::string str;
public:
MyClass(int value, std::string string):a(value),str(string){}
}
auto a = std::make_shared<MyClass>(10, "hello");
auto b = a; //请思考这个操作会发生什么?
1 初始状态:执行auto shardPtr = std::make_shared<MyClass>(10, "hello");后
sharedPtr的内存布局如下:
Stack:
a (shared_ptr对象)
|--> ptr指针 --------→ [Heap: MyClass对象(10,"hello")]
|--> control block指针 → [Heap: 控制块]
|- use_count = 1
|- weak_count = 1
|- deleter
|- allocator
2 拷贝构造过程:auto b = a;
std::shared_ptr<T>类实现了拷贝构造函数,因为a是一个左值,所以会触发拷贝构造函数的调用;shared_ptr的拷贝构造函数简化实现如下所示:
// 简化的拷贝构造函数实现
template<typename T>
shared_ptr<T>::shared_ptr(const shared_ptr& other) noexcept
{
this->ptr = other.ptr; // 复制指向管理对象的指针
this->control_block = other.control_block; // 复制指向控制块的指针
if (control_block) {
control_block->use_count++; // 原子地增加引用计数
}
}
从上面的简化实现看到,新的shared_ptr对象b仅仅是将a对象的两个指针拷贝到自己的对象中,所以说这两个对象指向的区域完全相同。
内部布局的变化:
Before:
Stack:
a (shared_ptr对象)
|--> ptr指针 --------→ [Heap: MyClass(10,"hello")]
|--> control block指针 → [Heap: 控制块]
|- use_count = 1
|- weak_count = 1
|- deleter
|- allocatorAfter:
Stack:
a (shared_ptr对象)
|--> ptr指针 -------------------→ [Heap:MyClass(10,"hello")]
|--> control block指针 ─┐ ↑
↓ | ↑
b (shared_ptr对象) | ↑ [Heap: 控制块]
|--> ptr指针 -----------------→↑ |- use_count = 2
|--> control block指针 ─┘ |- weak_count = 1
|- deleter
|- allocator
可以看到两个shared_ptr的ptr指针指向同一块数据区域;control block指针也是指向同一个数据控制块。
线程安全性
在shared_ptr对象中核心是修改引用技术,c++对引用计数的修改是原子操作:
// 在控制块中的引用计数增加是原子操作
atomic_increment(&control_block->use_count);
该原子操作确保了在多线程环境下使用shared_ptr对象。也就是说在不同的线程中操作同一个shared_ptr是不需要加锁的。
// 在不同线程中同时拷贝是安全的
void thread1() {
auto b = a; // 安全
}
void thread2() {
auto c = a; // 安全
}
总结
- std::shared_ptr对象是需要额外占用内存空间的,对象大小一般为2个指针变量的长度
- shared_ptr对象包含的是两个指针变量
- 当shard_ptr对象数据库块指针的引用变量变为0时,自动触发ptr指向的数据块的内存释放