导读:本期聚焦于小伙伴创作的《C++中std::shared_ptr如何实现延迟初始化?C++智能指针双重检查锁模式安全用法是什么》,敬请观看详情,探索知识的价值。以下视频、文章将为您系统阐述其核心内容与价值。如果您觉得《C++中std::shared_ptr如何实现延迟初始化?C++智能指针双重检查锁模式安全用法是什么》有用,将其分享出去将是对创作者最好的鼓励。

在C++多线程编程中,std::shared_ptr的延迟初始化是常见需求,既可以减少程序启动时的资源开销,也能避免不必要的对象构造。但多线程并发访问时,如果初始化逻辑没有做好同步,很容易出现重复初始化、内存泄漏或者访问未完全构造对象的问题,双重检查锁模式是优化同步开销的常用手段,不过需要结合std::shared_ptr的特性做正确实现。

C++中std::shared_ptr如何实现延迟初始化?C++智能指针双重检查锁模式安全用法是什么

为什么需要延迟初始化std::shared_ptr

延迟初始化的核心目标是把对象的构造时机从程序启动或者对象创建时,推迟到第一次真正需要使用该对象的时候。对于std::shared_ptr管理的资源来说,延迟初始化有这些好处:

  • 减少程序启动时的资源占用,避免提前构造暂时用不到的大对象
  • 如果对象构造可能失败,延迟初始化可以把失败处理放在使用处,逻辑更清晰
  • 对于单例场景或者全局共享资源,延迟初始化可以避免初始化顺序带来的问题

传统双重检查锁的问题

很多开发者首先会想到用双重检查锁来实现线程安全的延迟初始化,传统写法如下:

#include <memory>
#include <mutex>

std::shared_ptr<int> g_ptr;
std::mutex g_mutex;

std::shared_ptr<int> get_instance() {
    if (!g_ptr) { // 第一次检查,避免每次都加锁
        std::lock_guard<std::mutex> lock(g_mutex);
        if (!g_ptr) { // 第二次检查,加锁后确认仍未初始化
            g_ptr = std::make_shared<int>(100);
        }
    }
    return g_ptr;
}

这种写法看似没有问题,但实际上存在安全隐患。因为g_ptr = std::make_shared<int>(100)这个操作不是原子的,它分为两步:首先在堆上构造int对象并分配控制块,然后将g_ptr的内部指针指向这个对象。在没有内存屏障的情况下,其他线程可能看到g_ptr已经不为空,但指向的对象还没有完成构造,此时访问这个指针就会出现未定义行为。

安全的双重检查锁实现方案

要解决上面的问题,核心是保证std::shared_ptr的赋值操作对其他线程可见时,对象已经完全构造完成。C++11之后标准已经保证了std::shared_ptr的构造函数和赋值操作的相关同步语义,我们可以结合std::call_once或者正确的内存顺序来实现安全版本。

方案一:使用std::call_once实现

std::call_once是C++标准库提供的单次调用工具,本身就保证了线程安全和正确的内存同步,实现起来更简单也更安全:

#include <memory>
#include <mutex>

std::shared_ptr<int> g_ptr;
std::once_flag g_flag;

std::shared_ptr<int> get_instance() {
    std::call_once(g_flag, []() {
        g_ptr = std::make_shared<int>(100);
    });
    return g_ptr;
}

这种方案不需要手动写双重检查逻辑,std::call_once会保证初始化逻辑只执行一次,并且初始化完成后的结果对其他线程立即可见,完全避免了竞态条件。

方案二:正确的双重检查锁实现

如果一定要使用双重检查锁模式,需要保证指针赋值的同步语义,正确的实现如下:

#include <memory>
#include <mutex>
#include <atomic>

// 使用原子shared_ptr,C++20引入,之前的版本可以用atomic_load/atomic_store
std::atomic<std::shared_ptr<int>> g_atomic_ptr;
std::mutex g_mutex;

std::shared_ptr<int> get_instance() {
    // 原子加载,获取当前指针
    std::shared_ptr<int> local_ptr = std::atomic_load(&g_atomic_ptr);
    if (!local_ptr) {
        std::lock_guard<std::mutex> lock(g_mutex);
        local_ptr = std::atomic_load(&g_atomic_ptr);
        if (!local_ptr) {
            local_ptr = std::make_shared<int>(100);
            // 原子存储,保证其他线程能看到完整的对象
            std::atomic_store(&g_atomic_ptr, local_ptr);
        }
    }
    return local_ptr;
}

这里使用std::atomic包装std::shared_ptr,通过std::atomic_loadstd::atomic_store进行读写,这两个操作会插入必要的内存屏障,保证对象构造完成后再被其他线程看到,从而解决传统双重检查锁的内存可见性问题。

两种方案的选择建议

实际开发中更推荐使用std::call_once的方案,原因有两点:

  • 代码更简洁,不需要手动处理原子操作和锁的配合,出错概率更低
  • 标准库保证std::call_once的行为,可移植性更好,不需要依赖编译器对std::shared_ptr原子操作的支持

如果是在不支持C++20且无法使用std::atomic<std::shared_ptr>的旧环境中,也可以手动实现基于互斥锁的普通双重检查,但要保证初始化完成后才赋值给共享的std::shared_ptr,并且赋值操作在锁的保护范围内,不过这种方式的同步开销会比原子操作稍大。

注意事项

使用延迟初始化的std::shared_ptr时还要注意几个问题:

  • 延迟初始化的对象生命周期要符合预期,避免提前释放导致悬空指针
  • 如果初始化可能失败,要在初始化逻辑中处理失败情况,避免返回空的std::shared_ptr给调用方
  • 不要在析构函数中做延迟初始化的反向操作,避免析构顺序带来的问题
需要特别注意的是,普通的std::shared_ptr读写不是线程安全的,只有对同一个std::shared_ptr实例的并发读是安全的,并发写或者同时读写都需要同步,这也是我们上面方案中加入锁和原子操作的原因。

std::shared_ptr延迟初始化双重检查锁C++智能指针修改时间:2026-07-10 16:48:40

免责声明:​ 已尽一切努力确保本网站所含信息的准确性。网站内容多为原创整理与精心编撰,观点力求客观中立。本站旨在免费分享,内容仅供个人学习、研究或参考使用。若引用了第三方作品,版权归原作者所有。如内容涉及您的权益,请联系我们处理。
内容垂直聚焦
专注技术核心技术栏目,确保每篇文章深度聚焦于实用技能。从代码技巧到架构设计,为用户提供无干扰的纯技术知识沉淀,精准满足专业提升需求。
知识结构清晰
覆盖从开发到部署的全链路。AI、前端、编程、数据库、服务器、建站、系统层层递进,构建清晰学习路径,帮助用户系统化掌握开发与运维所需的核心技术。
深度技术解析
拒绝泛泛而谈,深入技术细节与实践难点。无论是数据库优化还是服务器配置,均结合真实场景与代码示例进行剖析,致力于提供可直接应用于工作的解决方案。
专业领域覆盖
精准对应开发生命周期。从前端界面到后端编程,从数据库操作到服务器运维,形成完整闭环,一站式满足全栈工程师和运维人员的技术需求。
即学即用高效
内容强调实操性,步骤清晰、代码完整。用户可根据教程直接复现和应用于自身项目,显著缩短从学习到实践的距离,快速解决开发中的具体问题。
持续更新保障
专注既定技术方向进行长期、稳定的内容输出。确保各栏目技术文章持续更新迭代,紧跟主流技术发展趋势,为用户提供经久不衰的学习价值。