优秀开源内容转自公众号后端开发成长指南

背景:在C++中,手动管理资源(new/delete)容易出错,导致:内存泄漏,重复释放,异常安全等问题。

为了解决这些问题,C++11引入了智能指针:

std::unique_ptr:独占所有权,std::shared_ptr 共享所有权,通过引用计数来管理生命周期

原理:

unique_ptr:目标是独占所有权,不可复制,只能移动(move)

使用场景:资源唯一所有权,RAII风格自动释放;

核心问题是:避免内存泄漏,支持移动语义。

核心原理:

1.独占所有权:唯一指针拥有资源,拷贝被禁用

2.移动语义:通过移动实现资源转移:

3.RAII:生命周期结束自动释放。

3.性能特点:

内存占用小(只存储原始指针和删除器)

无额外引用计数开销

编译器开销低

异常安全,必变资源泄漏;

shared_ptr:目标是多方共享资源,引用计数控制生命周期。

使用场景:多线程共享对象,缓存管理。

核心问题:

自动管理生命周期;处理多线程环境下的引用计数(atomic)

避免循环引用问题

1.内部结构:核心在于引用计数和控制块。

shared_ptr<T>

├── T* ptr // 实际对象指针

└── ControlBlock* cb // 引用计数和删除器

控制块结构示例

2.核心逻辑:

1)构造shared_ptr:初始化use_count = 1;

  1. 拷贝shared_ptr:use_count++(原子操作)

3)析构shared_ptr:use_count--,若为0调用删除器销毁对象;

weak_ptr:不增加use_count,仅增加weak_count,用于解决循环引用。

3.关键实现细节:

线程安全:std::atomic 保证引用计数在多线程下正确;

控制块分离:

对象和控制快分离,可以支持make_shared内联分配(减少内存碎片)

循环引用问题:

两个shared_ptr互相循环引用会导致引用计数不为0

需要weak_ptr解决;

4.性能分析:

unique_ptr vs shared_ptr

1.内存占用:

小,指针+删除器; 控制块额外开销,atomic计数

  1. 拷贝/移动开销

禁止拷贝,移动开销小; 拷贝需要原子操作,移动开销小

3.多线程安全

依赖外部保护; 引用计数原子操作保证安全

关键区别:引用计数的原子性

cpp

cpp 复制代码
// ❌ unique_ptr - 所有权转移不是线程安全的
class BadExample {
    std::unique_ptr<Data> data;
public:
    // 线程1调用
    void thread1() {
        auto temp = std::move(data); // 危险!没有同步
        // data现在是nullptr
    }
    
    // 线程2调用
    void thread2() {
        if (data) { // 竞态条件!
            data->process(); // 可能崩溃
        }
    }
};

// ✅ shared_ptr - 引用计数操作是原子的
class GoodExample {
    std::shared_ptr<Data> data;
public:
    // 线程1调用
    void thread1() {
        auto local_copy = data; // 原子地增加引用计数
        if (local_copy) {
            local_copy->process(); // 安全使用
        }
    } // 原子地减少引用计数
    
    // 线程2调用  
    void thread2() {
        auto local_copy = data; // 原子地增加引用计数
        if (local_copy) {
            local_copy->process(); // 安全使用
        }
    } // 原子地减少引用计数
};

更好的对比示例

cpp

复制代码
// unique_ptr - 必须用锁保护所有权转移
class TaskProcessor {
    std::unique_ptr<Task> current_task;
    std::mutex task_mutex;
public:
    void process() {
        std::unique_ptr<Task> local_task;
        {
            std::lock_guard<std::mutex> lock(task_mutex);
            local_task = std::move(current_task); // 必须在锁内移动
        }
        if (local_task) {
            local_task->execute();
        }
    }
    
    void setTask(std::unique_ptr<Task> task) {
        std::lock_guard<std::mutex> lock(task_mutex); // 必须加锁
        current_task = std::move(task);
    }
};

// shared_ptr - 复制操作本身是线程安全的
class DataCache {
    std::shared_ptr<CachedData> cached_data;
    // 注意:这里不需要mutex保护shared_ptr的复制!
public:
    void updateCache(std::shared_ptr<CachedData> new_data) {
        cached_data = new_data; // 原子操作,无需加锁
    }
    
    std::shared_ptr<CachedData> getData() {
        return cached_data; // 原子操作,无需加锁
    }
    
    void process() {
        auto local_data = getData(); // 线程安全地获取副本
        if (local_data) {
            // 即使其他线程调用updateCache(),local_data仍然有效
            local_data->doWork(); 
        }
    }
};

4.循环引用风险

无; 有,需要weak_ptr

5.适用场景:

独占资源 RAII; 多方共享对象管理,缓存,异步任务。

核心思想:能用 unique_ptr 就用 unique_ptr,能静态分析生命周期,避免 atomic 开销;需要多方共享才用 shared_ptr,并结合 weak_ptr 避免循环引用。

总结:

unique_ptr:

独占资源所有权,低开销,RAII自动释放

通过移动语义实现所有权转移

shared_ptr:共享资源,通过引用计数和控制块管理生命周期

支持多线程,但有性能开销

循环引用需要weak_ptr解决。

在分布式系统

hotspot 对象尽量用 unique_ptr

异步回调、共享上下文用 shared_ptr

一句话总结

"能唯一所有权就用 unique_ptr,必须共享就用 shared_ptr,控制引用计数就是控制你的性能。"

复制代码
struct ControlBlock {
    std::atomic<size_t> use_count;   // 强引用计数
    std::atomic<size_t> weak_count;  // 弱引用计数
    Deleter deleter;                 // 删除器
    T* ptr;                          // 指向对象
};
相关推荐
wangjialelele6 小时前
Linux中的线程
java·linux·jvm·c++
英杰.王8 小时前
JVM实战-G1参数调优
jvm
不务专业的程序员--阿飞8 小时前
JVM无法分配内存
java·jvm·spring boot
Micrle_0078 小时前
jvm类加载过程
jvm
the beard10 小时前
JVM垃圾回收器深度解析:从Serial到G1,探索垃圾回收的艺术
java·jvm
歪歪10011 小时前
如何在SQLite中实现事务处理?
java·开发语言·jvm·数据库·sql·sqlite
过客随尘1 天前
生产环境OOM排障实战
jvm·后端
summer_west_fish1 天前
JVM实际内存占用
jvm
Moshow郑锴1 天前
IDEA/WebStorm 卡顿问题与启动参数调优指南
java·jvm·intellij-idea·webstorm
三傻3172 天前
JVM 的垃圾处理机制
jvm