015 C++的shared_ptr


作者Lou Xiao, deepseek创建时间2025-04-02 14:16:16更新时间2025-04-02 14:16:16

1. 基本概念

1.1 智能指针概述

  • 智能指针是C++ RAII(资源获取即初始化)机制的典型应用
  • 自动管理动态分配的内存,防止内存泄漏
  • 标准库提供的主要智能指针:std::shared_ptr, std::unique_ptr, std::weak_ptr

1.2 std::shared_ptr 定义

  • 共享所有权的智能指针
  • 多个shared_ptr可以指向同一个对象
  • 通过引用计数机制管理对象的生命周期
  • 当最后一个shared_ptr离开作用域时,自动销毁管理的对象

2. 核心特性

2.1 引用计数机制

  • 每个shared_ptr管理的对象都有一个关联的引用计数器
  • shared_ptr被拷贝时,引用计数增加
  • shared_ptr被销毁或重置时,引用计数减少
  • 引用计数为0时,自动删除管理的对象

2.2 线程安全性

  • 引用计数的修改是原子操作,线程安全
  • 但管理的对象本身不提供线程安全保证
  • 多个线程同时访问同一个对象仍需同步机制

2.3 自定义删除器

  • 可以指定自定义的删除器代替delete
  • 适用于管理非内存资源或需要特殊清理的对象
  • 语法:std::shared_ptr<T> ptr(pointer, deleter)

3. 基本用法

3.1 创建shared_ptr

1.双击鼠标左键复制此行;2.单击复制所有代码。
                                
                                    
1 // 方式1:直接构造
2 std::shared_ptr<int> p1(new int(42));
3
4 // 方式2:使用make_shared (推荐)
5 std::shared_ptr<int> p2 = std::make_shared<int>(42);
6
7 // 方式3:拷贝构造
8 std::shared_ptr<int> p3 = p2;
9
10 // 方式4:自定义删除器
11 std::shared_ptr<FILE> filePtr(fopen("test.txt", "r"), [](FILE* f) {
12 if(f) fclose(f);
13 });

3.2 常用操作

1.双击鼠标左键复制此行;2.单击复制所有代码。
                                
                                    
1 // 解引用
2 *p1 = 10;
3
4 // 访问成员
5 std::shared_ptr<std::string> strPtr = std::make_shared<std::string>("hello");
6 strPtr->size();
7
8 // 获取原始指针
9 int* rawPtr = p1.get();
10
11 // 重置指针
12 p1.reset(); // 释放当前对象,变为空指针
13 p1.reset(new int(100)); // 指向新对象
14
15 // 检查是否唯一所有者
16 if(p1.unique()) { /* ... */ }
17
18 // 交换两个shared_ptr
19 std::shared_ptr<int> p4 = std::make_shared<int>(50);
20 p1.swap(p4);

4. 高级特性

4.1 std::make_shared的优势

  • 一次性分配内存,效率更高
  • 减少内存碎片
  • 异常安全
  • 语法:auto ptr = std::make_shared<T>(args...)

4.2 类型转换

1.双击鼠标左键复制此行;2.单击复制所有代码。
                                
                                    
1 struct Base { virtual ~Base() = default; };
2 struct Derived : public Base {};
3
4 std::shared_ptr<Derived> dptr = std::make_shared<Derived>();
5
6 // 静态转换
7 std::shared_ptr<Base> bptr = std::static_pointer_cast<Base>(dptr);
8
9 // 动态转换
10 std::shared_ptr<Derived> dptr2 = std::dynamic_pointer_cast<Derived>(bptr);
11
12 // 常量转换
13 std::shared_ptr<const Base> cptr = std::const_pointer_cast<const Base>(bptr);

4.3 循环引用问题

  • 当两个或多个shared_ptr互相引用时会导致内存泄漏
  • 解决方案:使用std::weak_ptr打破循环
1.双击鼠标左键复制此行;2.单击复制所有代码。
                                
                                    
1 struct Node {
2 std::shared_ptr<Node> next;
3 std::weak_ptr<Node> prev; // 使用weak_ptr避免循环引用
4 };

5. 性能考虑

5.1 开销分析

  • 内存开销:每个shared_ptr需要存储控制块(引用计数等)
  • 时间开销:引用计数的原子操作
  • 对比unique_ptrshared_ptr开销更大

5.2 使用场景

  • 适合使用shared_ptr的情况:
  • 多个对象需要共享所有权
  • 对象生命周期不明确
  • 需要将指针存入容器
  • 不适合的情况:
  • 单一所有权场景(应使用unique_ptr)
  • 性能关键路径
  • 需要明确生命周期管理的场景

6. 最佳实践

  1. 优先使用make_shared而非直接new
  2. 避免原始指针和shared_ptr混用
  3. 不推荐使用get()获取的原始指针创建新的shared_ptr
  4. 在可能产生循环引用的地方使用weak_ptr
  5. 在接口中明确所有权语义
  6. 避免不必要的shared_ptr拷贝
  7. 考虑使用std::enable_shared_from_thisthis创建shared_ptr

7. 常见陷阱

  1. 原始指针混用
1.双击鼠标左键复制此行;2.单击复制所有代码。
                                
                                    
1 int* raw = new int(10);
2 std::shared_ptr<int> p1(raw);
3 std::shared_ptr<int> p2(raw); // 错误!会导致双重释放
  1. 循环引用导致内存泄漏
1.双击鼠标左键复制此行;2.单击复制所有代码。
                                
                                    
1 struct A {
2 std::shared_ptr<B> b;
3 };
4 struct B {
5 std::shared_ptr<A> a;
6 };
7
8 auto a = std::make_shared<A>();
9 auto b = std::make_shared<B>();
10 a->b = b;
11 b->a = a; // 循环引用,内存泄漏
  1. this创建shared_ptr
1.双击鼠标左键复制此行;2.单击复制所有代码。
                                
                                    
1 struct Bad {
2 std::shared_ptr<Bad> getptr() {
3 return std::shared_ptr<Bad>(this); // 错误!
4 }
5 };
6 // 正确做法:继承std::enable_shared_from_this
  1. 数组误用
1.双击鼠标左键复制此行;2.单击复制所有代码。
                                
                                    
1 // 错误:shared_ptr默认使用delete而非delete[]
2 std::shared_ptr<int> arr(new int[10]);
3
4 // 正确:提供数组删除器
5 std::shared_ptr<int> arr(new int[10], [](int* p) { delete[] p; });
6 // 或使用C++17的shared_ptr<T[]>

8. C++17/20增强

8.1 C++17新特性

  • shared_ptr支持数组类型:std::shared_ptr<T[]>
  • std::reinterpret_pointer_cast
  • std::make_shared支持数组

8.2 C++20新特性

  • std::atomic<std::shared_ptr> (并发TS)
  • std::shared_ptrstd::weak_ptr的原子操作

9. 与其他智能指针比较

特性shared_ptrunique_ptrweak_ptr
所有权共享独占
引用计数
可复制
可移动
管理数组C++17支持支持不直接支持
循环引用解决方案需配合weak_ptr无此问题专门解决

10. 总结

std::shared_ptr是C++中实现共享所有权的核心工具,通过引用计数自动管理对象生命周期。正确使用时能极大简化内存管理,但需要注意循环引用、性能开销等问题。在设计中应明确所有权语义,优先考虑unique_ptr,仅在需要共享所有权时使用shared_ptr


附录: std::shared_ptr 的常用 API 表格

名称用途原型代码示例说明
构造函数创建 shared_ptrshared_ptr();
shared_ptr(T* ptr);
shared_ptr(nullptr_t);
std::shared_ptr<int> p1;
std::shared_ptr<int> p2(new int(42));
默认构造或从裸指针构造。注意避免直接传递 new 的结果(推荐 make_shared)。
make_shared安全创建 shared_ptrtemplate<class T, class... Args>
shared_ptr<T> make_shared(Args&&... args);
auto p = std::make_shared<int>(42);一次性分配内存和控制块,避免内存泄漏风险。
析构函数释放资源~shared_ptr();自动调用引用计数减 1,计数为 0 时销毁对象。
operator=赋值shared_ptr& operator=(const shared_ptr& r);p1 = p2;共享所有权,引用计数更新。
reset重置指针void reset();
void reset(T* ptr);
p.reset();
p.reset(new int(10));
释放当前所有权,可替换为新指针(或置空)。
get获取裸指针T* get() const;int* raw = p.get();不改变引用计数,谨慎使用以避免悬垂指针。
operator*解引用T& operator*() const;int val = *p;访问托管对象。
operator->成员访问T* operator->() const;p->method();访问托管对象的成员。
use_count获取引用计数long use_count() const;cout << p.use_count();主要用于调试,性能可能不高。
unique检查是否独占所有权bool unique() const;if (p.unique()) { ... }等价于 use_count() == 1(C++17 后弃用)。
swap交换指针void swap(shared_ptr& r);p1.swap(p2);高效交换两个 shared_ptr 的所有权。
owner_before提供弱序比较template<class U>
bool owner_before(const shared_ptr<U>& other) const;
if (p1.owner_before(p2)) { ... }用于关联容器中的排序,不比较对象值。
别名构造函数共享对象但指向其子部分shared_ptr(const shared_ptr<U>& r, T* ptr);std::shared_ptr<int> p_alias(p, &p->data);共享原指针的引用计数,但指向不同地址。
自定义删除器指定自定义资源释放逻辑shared_ptr(T* ptr, Deleter d);std::shared_ptr<FILE> fp(fopen("a.txt", "r"), fclose);删除器在引用计数为 0 时调用。
类型转换动态/静态指针转换template<class T, class U>
shared_ptr<T> dynamic_pointer_cast(const shared_ptr<U>& r);
auto derived = std::dynamic_pointer_cast<Derived>(base);类似 dynamic_cast,但返回 shared_ptr

关键注意事项:

  1. 避免循环引用:可能导致内存泄漏,需配合 std::weak_ptr 使用。
  2. 线程安全:引用计数操作是原子的,但对象访问需额外同步。
  3. 性能:控制块分配可能带来开销,优先使用 make_shared

如果需要更详细的特定场景说明或扩展 API(如 atomic_shared_ptr),可以进一步补充。

文章目录