文章目录
一、shared_ptr
std::shared_ptr
是 C++11 引入的另一种智能指针,与 unique_ptr
的独占所有权不同,它实现了共享所有权语义。多个 shared_ptr
实例可以同时管理同一个对象,当最后一个持有该对象的 shared_ptr
被销毁时,对象才会被自动释放。这种机制通过引用计数(reference counting)实现,是解决资源共享问题的理想选择。
核心优势
- 共享所有权:支持多指针共同管理同一资源
- 自动释放:最后一个所有者销毁时自动释放资源
- 灵活性:可与标准容器和算法无缝配合
- 线程安全:引用计数的修改是原子操作,支持多线程环境
- 自定义删除器:支持自定义资源释放逻辑
二、shared_ptr 原理
2.1 引用计数机制
shared_ptr
的核心是引用计数(reference count):
- 每个被管理的对象都有一个关联的引用计数器,记录当前有多少个
shared_ptr
指向它 - 当创建新的
shared_ptr
拷贝时,引用计数增加 1 - 当
shared_ptr
被销毁或重置时,引用计数减少 1 - 当引用计数变为0时,对象被自动删除
2.2 控制块结构
shared_ptr
内部通过控制块(control block)管理引用计数和其他资源。一个典型的控制块包含:
- 共享引用计数:跟踪管理对象的
shared_ptr
数量 - 弱引用计数:跟踪观察对象的
weak_ptr
数量(后续讲解) - 删除器:用于释放管理对象的函数或函数对象
- 分配器:用于内存管理的分配器
- 管理对象指针:指向实际管理的对象
2.3 内存布局
shared_ptr
通常包含两个指针大小的成员:
- 指向管理对象的指针(get() 返回的值)
- 指向控制块的指针
![shared_ptr内存布局示意图]
当使用 std::make_shared
创建 shared_ptr
时,控制块和管理对象会在同一块内存中分配,减少内存碎片并提高缓存效率。
2.4 简化版 shared_ptr 实现
下面通过实现简化版 shared_ptr
理解其工作原理:
#include <atomic>
#include <utility>
// 前向声明
template <typename T>
class WeakPtr;
// 控制块基类
class ControlBlockBase {
public:
std::atomic<int> shared_count; // 共享引用计数
std::atomic<int> weak_count; // 弱引用计数
ControlBlockBase() : shared_count(1), weak_count(0) {}
virtual ~ControlBlockBase() = default;
virtual void destroy() = 0; // 销毁管理对象
virtual void deallocate() = 0; // 释放控制块
};
// 针对原始指针的控制块
template <typename T, typename Deleter>
class ControlBlock : public ControlBlockBase {
private:
T* ptr; // 管理对象指针
Deleter deleter; // 删除器
public:
ControlBlock(T* p, Deleter d) : ptr(p), deleter(std::move(d)) {}
void destroy() override {
deleter(ptr); // 使用删除器销毁对象
}
void deallocate() override {
delete this; // 释放控制块自身
}
};
// 简化版 shared_ptr 实现
template <typename T>
class SharedPtr {
private:
T* ptr; // 存储的指针
ControlBlockBase* control_block; // 控制块指针
// 增加引用计数
void increase_count() {
if (control_block) {
control_block->shared_count.fetch_add(1, std::memory_order_relaxed);
}
}
// 减少引用计数并检查是否需要销毁
void decrease_count() {
if (control_block) {
if (control_block->shared_count.fetch_sub(1, std::memory_order_acq_rel) == 1) {
control_block->destroy(); // 销毁管理对象
if (control_block->weak_count == 0) {
control_block->deallocate(); // 释放控制块
}
}
}
}
public:
// 默认构造函数
SharedPtr() : ptr(nullptr), control_block(nullptr) {}
// 接受原始指针和删除器的构造函数
template <typename Deleter = std::default_delete<T>>
explicit SharedPtr(T* p, Deleter deleter = Deleter()) {
if (p) {
ptr = p;
control_block = new ControlBlock<T, Deleter>(p, std::move(deleter));
} else {
ptr = nullptr;
control_block = nullptr;
}
}
// 拷贝构造函数
SharedPtr(const SharedPtr& other) : ptr(other.ptr), control_block(other.control_block) {
increase_count();
}
// 移动构造函数
SharedPtr(SharedPtr&& other) noexcept : ptr(other.ptr), control_block(other.control_block) {
other.ptr = nullptr;
other.control_block = nullptr;
}
// 析构函数
~SharedPtr() {
decrease_count();
}
// 拷贝赋值运算符
SharedPtr& operator=(const SharedPtr& other) {
if (this != &other) {
decrease_count(); // 减少当前引用计数
ptr = other.ptr;
control_block = other.control_block;
increase_count(); // 增加新的引用计数
}
return *this;
}
// 移动赋值运算符
SharedPtr& operator=(SharedPtr&& other) noexcept {
if (this != &other) {
decrease_count(); // 减少当前引用计数
ptr = other.ptr;
control_block = other.control_block;
other.ptr = nullptr;
other.control_block = nullptr;
}
return *this;
}
// 解引用运算符
T& operator*() const {
return *ptr;
}
// 成员访问运算符
T* operator->() const {
return ptr;
}
// 数组访问运算符 (C++17)
T& operator[](size_t index) const {
return ptr[index];
}
// 获取原始指针
T* get() const {
return ptr;
}
// 获取引用计数
long use_count() const {
if (control_block) {
return control_block->shared_count.load(std::memory_order_relaxed);
}
return 0;
}
// 检查是否是唯一所有者
bool unique() const {
return use_count() == 1;
}
// 显式转换为 bool
explicit operator bool() const {
return ptr != nullptr;
}
// 重置指针
template <typename Deleter = std::default_delete<T>>
void reset(T* p = nullptr, Deleter deleter = Deleter()) {
SharedPtr<T>(p, std::move(deleter)).swap(*this);
}
// 交换指针
void swap(SharedPtr& other) noexcept {
std::swap(ptr, other.ptr);
std::swap(control_block, other.control_block);
}
// 友元类声明
friend class WeakPtr<T>;
};
// 辅助函数:创建 shared_ptr
template <typename T, typename... Args>
SharedPtr<T> make_shared(Args&&... args) {
return SharedPtr<T>(new T(std::forward<Args>(args)...));
}
2.5 关键技术点解析
-
引用计数的原子操作
- 使用
std::atomic
确保引用计数的线程安全 - 增加计数使用
memory_order_relaxed
(仅需原子性) - 减少计数使用
memory_order_acq_rel
(确保资源释放的正确顺序)
- 使用
-
控制块的多态设计
- 基类
ControlBlockBase
定义通用接口 - 派生类
ControlBlock
处理具体类型和删除器 - 支持不同类型的删除器和分配器
- 基类
-
拷贝与移动语义
- 拷贝操作增加引用计数
- 移动操作转移所有权,不修改引用计数
- 析构时减少计数,为0时销毁对象
三、shared_ptr 使用详解
3.1 基本用法
#include <memory>
#include <iostream>
struct MyClass {
MyClass(int id) : id(id) {
std::cout << "MyClass(" << id << ") constructed\n";
}
~MyClass() {
std::cout << "MyClass(" << id << ") destroyed\n";
}
int id;
};
int main() {
// 创建 shared_ptr (三种方式)
auto sp1 = std::make_shared<MyClass>(1); // 推荐:高效,异常安全
std::shared_ptr<MyClass> sp2(new MyClass(2)); // 不推荐:二次分配
auto sp3 = std::shared_ptr<MyClass>(sp2); // 拷贝构造,引用计数变为2
std::cout << "sp1 use count: " << sp1.use_count() << "\n"; // 1
std::cout << "sp2 use count: " << sp2.use_count() << "\n"; // 2
std::cout << "sp3 use count: " << sp3.use_count() << "\n"; // 2
// 共享所有权
{
auto sp4 = sp2; // 新的拷贝,引用计数变为3
std::cout << "Inside scope, use count: " << sp4.use_count() << "\n"; // 3
} // sp4 销毁,引用计数回到2
// 重置指针
sp2.reset(); // sp2 不再拥有对象,引用计数变为1
std::cout << "After sp2 reset, sp3 use count: " << sp3.use_count() << "\n"; // 1
return 0;
} // sp1, sp3 销毁,引用计数变为0,对象被销毁
输出结果:
MyClass(1) constructed
MyClass(2) constructed
sp1 use count: 1
sp2 use count: 2
sp3 use count: 2
Inside scope, use count: 3
After sp2 reset, sp3 use count: 1
MyClass(1) destroyed
MyClass(2) destroyed
3.2 自定义删除器
shared_ptr
支持自定义删除器,用于非默认的资源释放逻辑:
#include <cstdio>
// 自定义文件删除器
struct FileDeleter {
void operator()(FILE* fp) const {
if (fp) {
std::fclose(fp);
std::cout << "File closed\n";
}
}
};
int main() {
// 使用自定义删除器管理文件指针
std::shared_ptr<FILE> file_ptr(
std::fopen("example.txt", "w"),
FileDeleter()
);
if (file_ptr) {
std::fputs("Hello, shared_ptr!", file_ptr.get());
}
// 离开作用域时自动调用 FileDeleter 关闭文件
return 0;
}
也可使用 lambda 表达式作为删除器:
auto deleter = [](MyClass* p) {
std::cout << "Custom deleter for MyClass(" << p->id << ")\n";
delete p;
};
std::shared_ptr<MyClass> sp(new MyClass(3), deleter);
3.3 数组支持 (C++17)
C++17 开始支持管理动态数组:
// C++17 数组支持
auto arr_ptr = std::make_shared<int[]>(5); // 创建包含5个int的数组
// 访问数组元素
for (int i = 0; i < 5; ++i) {
arr_ptr[i] = i * 10;
}
// 数组版本会自动使用 delete[] 释放资源
3.4 与 weak_ptr 配合使用
shared_ptr
的最大问题是可能产生循环引用,此时需要 weak_ptr
打破循环:
#include <memory>
struct Node {
int data;
std::shared_ptr<Node> next; // 共享指针导致循环引用
// std::weak_ptr<Node> next; // 使用弱指针打破循环
};
int main() {
auto node1 = std::make_shared<Node>(1);
auto node2 = std::make_shared<Node>(2);
node1->next = node2;
node2->next = node1; // 形成循环引用
// 此时 node1 和 node2 的引用计数都是2,离开作用域后不会减为0
// 导致内存泄漏!
return 0;
}
解决方法是将其中一个 shared_ptr
改为 weak_ptr
:
struct Node {
int data;
std::weak_ptr<Node> next; // 弱指针不增加引用计数
};
weak_ptr
特性:
- 不拥有对象所有权,不增加引用计数
- 可通过
lock()
方法获取shared_ptr
(如果对象存在) - 用于解决循环引用问题
- 可通过
expired()
检查对象是否已被销毁
四、高级应用场景
4.1 多线程共享资源
shared_ptr
的引用计数操作是线程安全的,可以安全地在多线程间传递:
#include <thread>
#include <vector>
void func(std::shared_ptr<MyClass> sp) {
// 线程安全:引用计数的增减是原子操作
std::this_thread::sleep_for(std::chrono::milliseconds(10));
std::cout << "Thread " << std::this_thread::get_id()
<< ", use count: " << sp.use_count() << "\n";
}
int main() {
auto sp = std::make_shared<MyClass>(1);
std::vector<std::thread> threads;
// 创建5个线程共享 sp
for (int i = 0; i < 5; ++i) {
threads.emplace_back(func, sp);
}
for (auto& t : threads) {
t.join();
}
std::cout << "Main thread, use count: " << sp.use_count() << "\n";
return 0;
}
4.2 作为容器元素
shared_ptr
可安全存储在标准容器中:
#include <vector>
#include <algorithm>
int main() {
std::vector<std::shared_ptr<MyClass>> objects;
// 添加元素
objects.push_back(std::make_shared<MyClass>(1));
objects.push_back(std::make_shared<MyClass>(2));
objects.push_back(std::make_shared<MyClass>(3));
// 遍历并访问元素
std::for_each(objects.begin(), objects.end(), [](const auto& sp) {
std::cout << "MyClass id: " << sp->id << "\n";
});
return 0;
}
4.3 类型转换
shared_ptr
提供专门的类型转换函数,类似于 C++ 的强制类型转换:
struct Base { virtual ~Base() = default; };
struct Derived : Base { int value = 42; };
int main() {
std::shared_ptr<Base> base_ptr = std::make_shared<Derived>();
// 动态转换(安全检查)
if (auto derived_ptr = std::dynamic_pointer_cast<Derived>(base_ptr)) {
std::cout << "Derived value: " << derived_ptr->value << "\n";
}
// 静态转换(无安全检查)
auto derived_ptr = std::static_pointer_cast<Derived>(base_ptr);
std::cout << "Derived value: " << derived_ptr->value << "\n";
return 0;
}
五、注意事项与最佳实践
5.1 避免的操作
-
不要将原始指针交给多个 shared_ptr
// 错误示例:多个 shared_ptr 独立管理同一资源 int* raw_ptr = new int(10); std::shared_ptr<int> sp1(raw_ptr); std::shared_ptr<int> sp2(raw_ptr); // 严重错误!两个独立控制块,导致双重释放
-
避免循环引用
// 循环引用导致内存泄漏 struct A { std::shared_ptr<B> b; }; struct B { std::shared_ptr<A> a; }; auto a = std::make_shared<A>(); auto b = std::make_shared<B>(); a->b = b; b->a = a; // 循环引用,引用计数永远不为0
解决:将其中一个改为
std::weak_ptr
-
不要用 shared_ptr 管理栈上对象
// 错误示例:管理栈上对象 int x = 10; std::shared_ptr<int> sp(&x); // 析构时会调用 delete &x,导致未定义行为
5.2 最佳实践
-
优先使用 std::make_shared
- 优点1:一次分配内存(控制块+对象),效率更高
- 优点2:异常安全,避免资源泄漏
auto sp1 = std::make_shared<MyClass>(); // 推荐 auto sp2 = std::shared_ptr<MyClass>(new MyClass()); // 不推荐
-
避免使用 get() 返回的原始指针
- 不要将 get() 返回的指针交给另一个智能指针
- 不要手动释放 get() 返回的指针
-
正确处理数组(C++17前)
- C++17 前
shared_ptr
不直接支持数组,需提供自定义删除器
// C++17 前管理数组的正确方式 std::shared_ptr<int> arr_ptr(new int[5], std::default_delete<int[]>());
- C++17 前
-
使用 weak_ptr 解决循环引用
- 在双向链表、树等数据结构中,父节点持有子节点的
shared_ptr
,子节点持有父节点的weak_ptr
- 在双向链表、树等数据结构中,父节点持有子节点的
-
线程安全注意事项
shared_ptr
本身的引用计数操作是线程安全的- 但管理的对象不是线程安全的,仍需同步机制保护
六、shared_ptr 与 unique_ptr 对比
特性 | shared_ptr | unique_ptr |
---|---|---|
所有权 | 共享所有权 | 独占所有权 |
大小 | 两个指针大小 | 一个指针大小 |
引用计数 | 有 | 无 |
拷贝操作 | 允许(增加计数) | 禁止 |
移动操作 | 允许 | 允许 |
循环引用 | 可能(需 weak_ptr 解决) | 不可能 |
数组支持 | C++17 开始支持 | 原生支持 |
性能 | 引用计数操作有开销 | 无额外开销 |
选择建议:
- 当需要共享资源时,使用
shared_ptr
- 当资源仅需独占时,使用
unique_ptr
(性能更优) - 优先考虑
unique_ptr
,仅在必要时才使用shared_ptr
七、总结
std::shared_ptr
是 C++ 中实现共享所有权的智能指针,通过引用计数机制允许多个指针共同管理同一资源。其核心特点包括:
- 共享所有权:多个
shared_ptr
可指向同一对象 - 自动释放:最后一个所有者销毁时释放资源
- 线程安全:引用计数操作是原子的,支持多线程环境
- 灵活性:支持自定义删除器和分配器
- 配合 weak_ptr:可解决循环引用问题
在实际开发中,应根据资源所有权需求选择合适的智能指针,优先考虑 unique_ptr
以获得更好的性能,仅在需要共享资源时使用 shared_ptr
。