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

背景:在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;                          // 指向对象
};
相关推荐
用户0942485680313 小时前
第18章:ConcurrentHashMap与并发容器实战
java·jvm
一条破秋裤17 小时前
Linux 线程分离与主动取消:pthread_detach、pthread_cancel
java·linux·jvm
bksczm20 小时前
零基础面试题1
jvm·数据结构·算法
今天的砖头有点烫手啊21 小时前
GC 算法与垃圾收集器演进:从 Serial 到 ZGC
java·jvm
johnny23321 小时前
JVM生态下Web开发框架:SparkJava、Javalin、Micronaut、SparkKotlin
jvm
晨小安1 天前
ThreadLocal及其内存泄漏问题深度解析
java·jvm·tomcat·哈希表
jingli92 天前
一个测试用例把连接池卡死 30 秒:SQLite 连接池 max_size=1 下的重入自锁复盘
jvm·sqlite·测试用例
imDwAaY2 天前
Java中垃圾回收器 G1 和 CMS 有什么区别?
jvm·后端
xiaoqiMikko2 天前
JVM 线上排查实战(五):升 JDK 17 后进程起不来,十几个老 GC 参数挨个实测(完结)
java·jvm
Flynt2 天前
Java 27 悄悄改了 3 个默认值,我在 1 核小机器上逐个验证了一遍
java·jvm·性能优化