找回密码
立即注册
搜索
热搜: Java Python Linux Go
发回帖 发新帖

837

积分

0

好友

103

主题
发表于 前天 22:14 | 查看: 4| 回复: 0

std::shared_ptr 是 C++11 引入的共享所有权智能指针,通过引用计数管理动态内存。多个 shared_ptr 可指向同一对象,当最后一个指向该对象的 shared_ptr 销毁、重置时,对象才会被自动释放。本文详细讲解其核心用法以及常见陷阱与解决方案。

核心用法

1. 基本创建方式

  • 优先使用 std::make_shared 创建:此方法将内存分配与对象构造合并,通常更高效且能提供更强的异常安全保障。
  • 避免直接用 new 构造:此方法容易导致忘记 delete 的内存泄漏,并且在复杂表达式求值顺序下存在异常不安全的风险。

图片

2. 用法示例

#include <iostream>
#include <memory>

int main(){
    // 1. 创建
    auto sp1 = std::make_shared<std::string>("hello");
    // 2. 共享所有权(引用计数+1)
    std::shared_ptr<std::string> sp2 = sp1;
    std::cout << "引用计数:" << sp1.use_count() << std::endl; // 输出 2
    // 3. 重置(释放所有权,引用计数-1)
    sp2.reset();
    std::cout << "引用计数:" << sp1.use_count() << std::endl; // 输出 1
    // 4. 解引用(访问对象)
    std::cout << *sp1 << std::endl; // 输出 hello
    // 5. 检查是否为空
    if (sp1) {
        std::cout << "sp1 非空" << std::endl;
    }
    // 6. 自定义删除器(适用于数组、文件句柄等非默认释放场景)
    // 示例:管理动态数组(shared_ptr 默认不支持数组,需自定义删除器)
    std::shared_ptr<int> sp_arr(new int[5]{1, 2, 3, 4, 5}, [](int* p) { delete[] p; });
    // C++17 后可直接用 shared_ptr<int[]> sp_arr(new int[5]);
    return 0;
}

3. 常用成员函数

图片

坑与避坑方案

坑1:循环引用(最致命!内存泄漏)

问题原理

两个(或多个)std::shared_ptr 互相引用,导致引用计数无法归0,对象永远不会被释放。

示例(父子节点循环引用)

#include <iostream>
#include <memory>

struct Node {
    std::shared_ptr<Node> parent; // 父节点
    std::shared_ptr<Node> child;  // 子节点
    ~Node() {
        // 不会执行!
        std::cout << "Node 析构" << std::endl;
    }
};

int main(){
    auto parent = std::make_shared<Node>();
    auto child  = std::make_shared<Node>();
    // parent 引用 child,child 计数=2
    parent->child = child;
    // child 引用 parent,parent 计数=2
    child->parent = parent;
    // 函数结束时,parent/child 销毁,
    // 计数各减1,父子节点均为1,对象无法析构
    return 0;
}
避坑方案

std::weak_ptr 打破循环引用。std::weak_ptr 是弱引用,不增加引用计数,仅观察 std::shared_ptr 指向的对象。

#include <iostream>
#include <memory>

struct Node {
    // 改为 weak_ptr
    std::weak_ptr<Node>   parent;
    std::shared_ptr<Node> child;
    ~Node() {
        // 正常执行
        std::cout << "Node 析构" << std::endl;
    }
};

int main(){
    auto parent = std::make_shared<Node>();
    auto child  = std::make_shared<Node>();
    parent->child = child;
    // weak_ptr 不增加 parent 的计数
    child->parent = parent;
    // parent 计数=1
    // child 计数=1
    // 函数结束后均析构
    return 0;
}

weak_ptr 主要方法说明

  • lock():将 weak_ptr 转为 shared_ptr,对象存活则返回有效 shared_ptr,否则返回 nullptr
  • expired():判断指向的对象是否已销毁。
  • use_count():返回关联 shared_ptr 的引用计数。

坑2:用原始指针创建多个 shared_ptr(重复释放)

问题场景

同一原始指针被多个 std::shared_ptr 独立管理,每个 std::shared_ptr 都会尝试释放该指针,导致双重释放,程序崩溃。

错误示例

int* raw_ptr = new int(10);
std::shared_ptr<int> sp1(raw_ptr); // 致命!sp1 和 sp2 各自管理 raw_ptr
std::shared_ptr<int> sp2(raw_ptr);
// 函数结束时,sp1 先释放 raw_ptr
// sp2 再释放,二次释放,程序会崩溃
避坑方案
  1. 绝对不要用同一原始指针创建多个 shared_ptr
  2. 若需共享所有权,通过 shared_ptr 拷贝,而非原始指针:
    auto sp1 = std::make_shared<int>(10);
    // 正确:引用计数+1,共享所有权
    std::shared_ptr<int> sp2 = sp1;
  3. 若必须使用原始指针(如:兼容旧代码),用 shared_ptrget() 获取,但绝不能通过 get() 的结果创建新的 shared_ptr

坑3:shared_ptr 管理非动态内存(栈内存、全局内存)

问题场景

shared_ptr 默认删除器是 delete,若指向栈内存或全局内存,销毁时会调用 delete 释放非堆内存,导致程序崩溃。

错误示例

int val = 10; // 栈内存
// 致命!sp 指向栈内存
std::shared_ptr<int> sp(&val);
// 函数结束时,sp 调用 delete &val
// 释放栈内存,崩溃
避坑方案
  1. shared_ptr 仅管理动态分配的内存(newnew[])或自定义资源(如:文件句柄)。
  2. 若需管理非动态资源,必须自定义删除器(空操作):
    int val = 10;
    // 删除器为空,不释放内存
    // 这么做有点多余,但是合法!
    std::shared_ptr<int> sp(&val, [](int*) {});

坑4:C++17前用shared_ptr 管理数组

问题场景

C++17前,shared_ptr 默认删除器是 delete,而非 delete[]。若管理动态数组,会导致数组元素析构不完整,引发内存泄漏或崩溃。

错误示例(C++17 前)

// 错误!默认删除器是 delete,不是 delete[]
std::shared_ptr<int> sp(new int[5]);
// 释放时仅删除第一个元素,其余4个元素内存泄漏
避坑方案
  1. C++17 及以上 直接用 shared_ptr<T[]> 管理数组(默认删除器为 delete[]):
    std::shared_ptr<int[]> sp(new int[5]{1,2,3,4,5});
  2. C++17 以下 需要自定义删除器:
    std::shared_ptr<int> sp(new int[5], [](int* p) { delete[] p; });
  3. 优先用 std::vector 替代动态数组,避免手动管理内存。

坑5:滥用 use_count() 导致性能与逻辑错误

问题原理
  1. use_count() 是相对昂贵的操作(通常涉及原子操作),频繁调用会降低性能。
  2. use_count() == 1 不保证“当前线程是唯一拥有者”(在多线程下,该值可能被其他线程瞬间修改),无法作为线程安全的唯一性判断依据。在进行多线程并发编程时,尤其需要注意这一点。

错误示例(多线程)

// 线程1
if (sp.use_count() == 1) {
    // 线程2可能在此处增加引用计数,导致逻辑错误
    modify_object(*sp);
}
避坑方案
  1. 仅在调试时使用 use_count(),生产环境避免依赖其返回值进行业务逻辑判断。
  2. 多线程下如需判断“是否唯一拥有者”,应通过互斥锁等同步机制保证 use_count() 检查和后续操作的原子性。
  3. 若必须依赖引用计数,考虑使用 std::atomic 变量或锁来保护相关逻辑。

坑6:shared_ptr 的拷贝、移动也是有开销的

问题场景

shared_ptr 拷贝时需要原子操作更新引用计数,其开销高于普通指针拷贝。在性能敏感的代码路径中,频繁拷贝会影响效率。

避坑方案
  1. 传递 shared_ptr 时,若无共享所有权的需求,优先使用 const 引用(const std::shared_ptr<T>&),避免不必要的拷贝。
  2. 若仅需临时访问对象,可使用 std::weak_ptr 或通过 get() 获取原始指针进行访问。
  3. 临时传递所有权时,使用 std::move(移动语义),移动操作不会更新引用计数,性能更高:
    void func(const std::shared_ptr<int>& sp) { /* ... */ }
    auto sp = std::make_shared<int>(10);
    // 移动:引用计数不变,sp 变为空
    func(std::move(sp));

坑7:直接用new构造shared_ptr,导致异常安全问题

问题场景

直接用 new 表达式构造 shared_ptr 时,若 shared_ptr 构造函数抛异常(如内存不足),new 分配的内存会泄漏,因为此时智能指针还未接管该内存。

错误示例

// 危险:若 shared_ptr 构造抛异常
// new int(10) 的内存泄漏
func(std::shared_ptr<int>(new int(10)));
避坑方案

强制使用 std::make_sharedstd::make_shared 是异常安全的,因为它在一个步骤中完成内存分配和对象构造,避免了 new 和智能指针构造之间的间隙可能引发的内存泄漏。

// 安全
func(std::make_shared<int>(10));

总结

最佳实践

  1. 创建优先:优先用 std::make_shared 创建 std::shared_ptr,避免直接用 new
  2. 打破循环:用 std::weak_ptr 打破循环引用,仅在需要访问对象时通过 lock() 转为 std::shared_ptr
  3. 数组管理:C++17之前避免用 std::shared_ptr 管理数组,优先用 std::vector 或自定义删除器。
  4. 传递优化:极致性能追求场景下,传递 std::shared_ptr 时用 const 引用,临时传递所有权用 std::move
  5. 原始指针隔离:绝不混用原始指针和 std::shared_ptr 创建多个管理者,仅通过 get() 临时访问。
  6. 线程安全注意std::shared_ptr 的引用计数更新是原子的,但对象的读写仍需加锁保护。
  7. 析构函数:避免在析构函数中操作 std::shared_ptr,可能导致循环引用或未定义行为。
  8. 原子操作:C++20后,若需在多线程间安全地替换 shared_ptr 本身,优先考虑 std::atomic<std::shared_ptr>

std::shared_ptr 是管理共享所有权内存的利器,其核心风险在于循环引用和重复释放。善用 std::weak_ptr 可打破循环引用,坚持使用 std::make_shared 能保证安全的内存创建。避免混用原始指针,就能规避大多数陷阱。在实际开发中,应优先考虑独占所有权的 std::unique_ptr,因其开销更低,仅在确需共享所有权时才使用 std::shared_ptr




上一篇:后端开发五年后,该走技术专家还是管理路线?
下一篇:Jenkins Pipeline最佳实践:构建高效安全的微服务自动化部署流水线
您需要登录后才可以回帖 登录 | 立即注册

手机版|小黑屋|网站地图|云栈社区 ( 苏ICP备2022046150号-2 )

GMT+8, 2025-12-17 13:11 , Processed in 0.105165 second(s), 40 queries , Gzip On.

Powered by Discuz! X3.5

© 2025-2025 云栈社区.

快速回复 返回顶部 返回列表