C++11 智能指针:`std::unique_ptr`、`std::shared_ptr`和`std::weak_ptr`

之前我研究了机器人开发中的 ROS2(Jazzy)系统相关内容。并将官网中比较重要的教程和概念,按照自己的学习顺序翻译成了中文,整理记录在了公众号里。在记录的过程中,我针对一些不太容易理解的部分进行了额外的研究和补充说明。到目前为止,已经整理了20多篇文章。如果你想回顾之前的内容,可以查阅公号主页中 ROS2(Jazzy)相关文章。

在研究 ROS2 的过程中,我发现它使用了不少 C++11 的新特性。这让我意识到,掌握这些特性对于深入理解 ROS2 的实现原理和优化代码非常重要。因此,我萌生了撰写 C++11 系列文章的想法。

目前已经完成了两篇 C++11 系列文章:

  1. ROS2性能狂飙:C++11移动语义'偷梁换柱'实战
  2. Lambda 表达式 以及 std::functionstd::bind

而本文是第三篇,主要总结的是 C++11 智能指针:std::unique_ptrstd::shared_ptrstd::weak_ptr

C++11 的 三种智能指针

C++11 引入了三种智能指针类型,它们位于 <memory> 头文件中,用于自动管理动态分配的内存,防止内存泄漏和悬空指针问题。

1. std::unique_ptr(独占所有权指针)

独占所有权指针std::unique_ptr的主要特点:

  • 独占资源所有权,同一时间只能有一个 unique_ptr 指向对象。
  • 资源不可以复制,不允许有拷贝构造和赋值,但是可以通过std::move支持移动语义。
  • 生命周期结束时自动释放资源,调用析构函数。

一般在资源独占管理时使用(如工厂模式返回对象)

cpp 复制代码
#include <memory>

void demo_unique_ptr() {
    // 创建 unique_ptr
    std::unique_ptr<int> p1(new int(10));
    // auto p1 = std::make_unique<int>(10); // C++14 更安全

    // 移动所有权(p1 变为空)
    std::unique_ptr<int> p2 = std::move(p1);

    // 重置资源(释放原有内存,接管新资源)
    p2.reset(new int(20));

    // 释放所有权并返回裸指针(需手动管理)
    int* raw_ptr = p2.release();
    delete raw_ptr;
} // 自动释放 p2(若未 release)

2. std::shared_ptr(共享所有权指针)

共享所有权指针std::shared_ptr的主要特点是:

  • 通过引用计数管理资源,多个 shared_ptr 可以共享同一对象。
  • 引用计数为 0 时自动释放资源。
  • 支持拷贝和移动语义。

适用于需要多个指针共享同一资源(如共享缓存)的场景。

cpp 复制代码
#include <memory>

void demo_shared_ptr() {
    // 创建 shared_ptr(引用计数=1)
    std::shared_ptr<int> p1(new int(5));
    // auto p1 = std::make_shared<int>(5); // 推荐:减少内存分配次数

    {
        // 共享所有权(引用计数=2)
        std::shared_ptr<int> p2 = p1;
        *p2 = 10; // 修改共享资源
    } // p2 析构,引用计数=1

    // 查看引用计数
    std::cout << "Count: " << p1.use_count() << std::endl; // 输出 1
} // p1 析构,引用计数=0,释放资源

3. std::weak_ptr(弱引用指针)

弱引用指针std::weak_ptr的特点是:

  • 不增加引用计数,不控制资源生命周期。
  • 解决 shared_ptr 循环引用导致的内存泄漏。
  • 需要通过 lock() 转换为 shared_ptr 以访问资源。 适用于打破循环引用(如观察者模式、缓存)的场景。
cpp 复制代码
#include <memory>

struct Node {
    std::shared_ptr<Node> next;
    std::weak_ptr<Node> prev; // 弱引用打破循环
};

void demo_weak_ptr() {
    auto node1 = std::make_shared<Node>();
    auto node2 = std::make_shared<Node>();

    node1->next = node2;
    node2->prev = node1; // 使用 weak_ptr 避免循环引用

    // 访问资源:提升为 shared_ptr
    if (auto locked = node2->prev.lock()) {
        // 安全使用 locked
    }
} // 资源正确释放

性能与开销

  • unique_ptr:几乎零开销(编译时处理所有权)。
  • shared_ptr:引用计数带来额外开销(原子操作)。
  • weak_ptr:需额外存储弱引用计数。

智能指针对比

智能指针是 C++ 资源管理的核心工具,正确使用可显著提升代码安全性与可维护性。

类型 所有权 拷贝/移动 适用场景
unique_ptr 独占 仅移动 单一所有者
shared_ptr 共享 支持 共享资源
weak_ptr 无(观察) 支持 打破循环引用、缓存

C++ 智能指针使用场景及高级技巧

一)核心使用场景

1. 资源所有权管理

在资源所有权管理场景中使用智能指针的核心原因主要是自动化资源生命周期管理

cpp 复制代码
// 文件资源管理
auto file = std::unique_ptr<FILE, decltype(&fclose)>(
    fopen("data.txt", "r"), fclose
);

// 网络套接字管理
auto socket = std::unique_ptr<Socket, SocketDeleter>(createSocket());

它主要解决以下关键问题:

1)避免资源泄漏(核心价值)
  • 文件描述符(FILE*)和套接字是有限的系统资源
  • 手动管理时极易忘记调用 fclose()closesocket()
  • 智能指针确保资源必然释放(即使发生异常)
cpp 复制代码
// 危险的手动管理
void processFile() {
    FILE* f = fopen("data.txt", "r");
    if(some_error) throw std::runtime_error("Oops"); // 资源泄漏!
    fclose(f); // 可能永远不会执行
}

// 安全的智能指针管理
void safeProcessFile() {
    auto file = std::unique_ptr<FILE, decltype(&fclose)>(fopen("data.txt", "r"), fclose);
    if(some_error) throw std::runtime_error("Safe"); // 自动调用fclose!
}
2)保证异常安全
  • C++异常会打断正常执行流程
  • 智能指针基于 RAII 原则,在栈展开时自动释放资源
  • try-catch-finally 更简洁可靠
3)明确所有权语义
  • unique_ptr 清晰表达"独占所有权"关系
  • 防止资源被多个实体意外共享
  • 避免悬空指针问题
cpp 复制代码
// 所有权不明确的手动管理
Socket* createSocket() {
    return new Socket(); // 调用者必须记得删除
}

// 明确的智能指针所有权
std::unique_ptr<Socket> createSafeSocket() {
    return std::unique_ptr<Socket>(new Socket()); // 所有权转移给调用者
}
2. 工厂模式(所有权转移)

在工厂模式中使用 std::unique_ptr 进行所有权转移是现代C++资源管理的核心实践。让我们逐层分析这个示例的所有权转移过程:

cpp 复制代码
class WidgetFactory {
public:
    std::unique_ptr<Widget> createWidget() {
        return std::make_unique<Widget>();
    }
};

// 客户端获得独占所有权
auto widget = factory.createWidget();
第一阶段:资源创建阶段(工厂内部)
cpp 复制代码
std::make_unique<Widget>()  // 在堆上创建Widget对象
  • 在堆上动态分配 Widget 对象
  • 创建临时 unique_ptr 拥有该对象的所有权
  • 此时所有权 :临时 unique_ptr
第二阶段:函数返回阶段(所有权第一次转移)
cpp 复制代码
return ...;  // 返回临时unique_ptr
  • 触发移动语义(非拷贝!)
  • 编译器优化(RVO或NRVO)可能直接构造返回值
  • 所有权转移 :临时 unique_ptr → 函数返回值
第三阶段:客户端接收阶段(所有权第二次转移)
cpp 复制代码
auto widget = factory.createWidget();
  • 函数返回值移动到 widget 变量
  • 最终所有权 :客户端 widget 变量
  • 原临时对象被置空(nullptr
那么为什么要这样设计呢?主要原因如下

1)为了明确所有权边界

  • 工厂:负责创建对象,不保留所有权
  • 客户端:获得对象,承担完全所有权
  • 防止工厂意外持有资源导致泄漏

2)防止资源泄漏

cpp 复制代码
// 危险的传统工厂
Widget* createWidget() {
   return new Widget();  // 可能泄漏!
}

// 安全调用
auto widget = factory.createWidget(); // 自动管理
  • 即使客户端忘记删除,智能指针也会自动释放
  • 异常安全保证:
cpp 复制代码
void process() {
   auto w = factory.createWidget();
   throw std::runtime_error("Oops"); // 仍会释放Widget!
}

3)为了支持多态安全

cpp 复制代码
class SpecialWidget : public Widget { ... };

std::unique_ptr<Widget> createWidget() {
    return std::make_unique<SpecialWidget>(); // 正确释放派生类
}
3. 共享数据缓存

在这个共享数据缓存实现中,std::shared_ptr 的使用解决了缓存系统的核心问题:如何在多个使用者之间安全共享数据,同时确保数据在不再需要时自动释放。

cpp 复制代码
class Cache {
    std::map<int, std::shared_ptr<Data>> cache; // 缓存存储
public:
    std::shared_ptr<Data> getData(int id) {
        // 1. 查找缓存
        if(auto it = cache.find(id); it != cache.end()) {
            return it->second; // 返回共享引用
        }
        
        // 2. 缓存未命中时加载数据
        auto data = std::make_shared<Data>(loadData(id));
        
        // 3. 存入缓存
        cache[id] = data;
        
        // 4. 返回数据
        return data;
    }
};

详细流程分析如下:

1) 首次请求(缓存未命中)

cpp 复制代码
auto data1 = cache.getData(100);
  • 创建新 shared_ptr(引用计数=1)
  • 存入缓存(引用计数=2)
  • 返回给客户端(引用计数=2)

2) 二次请求(缓存命中)

cpp 复制代码
auto data2 = cache.getData(100);
  • 从缓存返回相同指针
  • 引用计数增至3
  • data1data2 指向同一对象

3) 客户端释放

cpp 复制代码
data1.reset(); // 引用计数=2
data2.reset(); // 引用计数=1(仅缓存持有)

4)缓存清理

cpp 复制代码
cache.evict(100); // 移除条目,引用计数=0 → 自动释放

一般情况下我们使用 shared_ptr的核心原因主要有以下两点:

1) 共享所有权需求

  • 缓存需要持有数据(供后续请求使用)
  • 客户端也需要持有数据(直接使用)
  • 双方需要共享所有权,不能由单方独占

2) 自动生命周期管理

  • 当最后一个使用者释放数据时自动清理
  • 避免"缓存持有过期数据"或"提前释放"问题
4. 观察者模式(避免循环引用)

在这个观察者模式实现中,std::weak_ptr 的使用是解决 循环引用导致内存泄漏 问题的关键。让我们通过分解代码来理解其工作原理:

cpp 复制代码
class Observer {
    std::weak_ptr<Subject> subject_; // 关键:弱引用避免循环
    
public:
    void observe(std::shared_ptr<Subject> s) {
        subject_ = s; // 弱引用赋值
    }
    
    void notify() {
        // 尝试将弱引用提升为强引用
        if(auto s = subject_.lock()) { 
            s->update();
        }
    }
};

那么以上代码是是如何避免循环引用的呢?

首先我们来考虑一下如果不使用 weak_ptr 的危险实现:

cpp 复制代码
// 危险:循环引用导致内存泄漏!
class Observer {
    std::shared_ptr<Subject> subject_; // 强引用
    
public:
    void observe(std::shared_ptr<Subject> s) {
        subject_ = s; // 强引用赋值
    }
};

class Subject {
    std::vector<std::shared_ptr<Observer>> observers_; // 强引用观察者
};

SubjectObserver 互相持有对方的 shared_ptr 时,将导致以下问题:

  1. 引用计数永远不会归零
  2. 对象永远不会被释放
  3. 内存泄漏不可避免

因此我们使用 weak_ptr 作为解决方案

cpp 复制代码
std::weak_ptr<Subject> subject_; // 弱引用

这里的工作原理是这样的:

1)不增加引用计数

  • Observer 通过 subject_ = s 存储 Subject
  • Subject 的引用计数 不会增加
  • 打破循环引用链

2)安全访问机制

cpp 复制代码
if(auto s = subject_.lock()) { 
    s->update();
}
  • lock() 尝试将 weak_ptr 提升为 shared_ptr
  • 如果 Subject 仍存在 → 返回有效的 shared_ptr(临时增加引用计数)
  • 如果 Subject 已被释放 → 返回 nullptr

实际应用场景

1)GUI事件系统

cpp 复制代码
class Button : public Subject { /* ... */ };

class Tooltip : public Observer {
public:
    void notify() override {
        if(auto btn = button_.lock()) {
            showAt(btn->position());
        }
    }
};

// 使用
auto button = std::make_shared<Button>();
auto tooltip = std::make_shared<Tooltip>();
tooltip->observe(button);

2)游戏实体系统

cpp 复制代码
class Player : public Subject { /* 血量变化等 */ };

class HealthBar : public Observer {
public:
    void notify() override {
        if(auto player = player_.lock()) {
            updateBar(player->health());
        }
    }
};

3)网络重连机制

cpp 复制代码
class Connection : public Subject { /* 连接状态 */ };

class Reconnect : public Observer {
public:
    void notify() override {
        if(auto conn = connection_.lock()) {
            if(conn->isDisconnected()) {
                startReconnect();
            }
        }
    }
};

总之,在观察者模式中使用 weak_ptr好处有如下几个方面:

1) 根本解决 循环引用导致的内存泄漏问题 2) 提供安全机制 访问可能已释放的对象 3) 保持所有权清晰

  • 主体(Subject)拥有观察者列表
  • 观察者(Observer)仅观察主体,不拥有其生命周期 4) 支持灵活架构
  • 动态注册/注销观察者
  • 主体可安全释放而不影响观察者 5) 实现自清理:自动处理失效的观察关系

这种模式是现代C++实现观察者模式的黄金标准,广泛应用于:

  • GUI事件处理
  • 游戏对象系统
  • 消息通知机制
  • 网络状态监控
  • 实时数据流处理

正确使用 weak_ptr 可以构建既安全又高效的观察者系统,避免常见的内存管理陷阱。

二)高级技巧与实践

  1. 自定义删除器
cpp 复制代码
// 数据库连接管理
auto dbConn = std::shared_ptr<Database>(
    createDatabaseConnection(),
    [](Database* db) {
        db->close();
        delete db;
    }
);

// 内存池分配器
struct CustomDeleter {
    void operator()(Object* obj) {
        memoryPool.release(obj);
    }
};
std::unique_ptr<Object, CustomDeleter> objPtr;
  1. 安全的多态处理
cpp 复制代码
class Base {
public:
    virtual ~Base() = default;
};

class Derived : public Base {};

// 安全向下转型
std::shared_ptr<Base> base = std::make_shared<Derived>();
if(auto derived = std::dynamic_pointer_cast<Derived>(base)) {
    // 安全使用派生类
}
  1. 性能优化技巧
cpp 复制代码
// 1. 传递const引用避免计数操作
void process(const std::shared_ptr<Data>& data) {
    // 读取操作(不增加引用计数)
}

// 2. 移动语义优化
std::vector<std::unique_ptr<Widget>> widgets;
widgets.push_back(std::make_unique<Widget>()); // 移动而非拷贝

// 3. make_shared优化内存布局
auto sp = std::make_shared<LargeObject>(); // 单次内存分配
  1. API边界处理
cpp 复制代码
// 接收原始指针(不获取所有权)
void processData(const Data* data);

// 获取所有权
void takeOwnership(std::unique_ptr<Data> data);

// 共享所有权
void shareOwnership(std::shared_ptr<Data> data);

三)特殊场景处理

  1. 处理循环引用
cpp 复制代码
struct TreeNode {
    std::weak_ptr<TreeNode> parent; // 关键:弱引用
    std::vector<std::shared_ptr<TreeNode>> children;
};

auto root = std::make_shared<TreeNode>();
auto child = std::make_shared<TreeNode>();
child->parent = root; // 弱引用赋值
root->children.push_back(child);
  1. 从this创建shared_ptr
cpp 复制代码
class Session : public std::enable_shared_from_this<Session> {
public:
    void start() {
        // 安全获取自身的shared_ptr
        auto self = shared_from_this();
        asyncOperation([self] { 
            self->processResult(); 
        });
    }
};
  1. 兼容C接口
cpp 复制代码
// 将智能指针释放为裸指针(转移所有权)
std::unique_ptr<Data> dataPtr = ...;
process_c_api(dataPtr.release());

// 从C API接收所有权
std::unique_ptr<Data> wrap_c_data(Data* raw) {
    return std::unique_ptr<Data>(raw);
}

四)C++17/20增强技巧

  1. 数组支持(C++17)
cpp 复制代码
// 创建共享数组
auto arr = std::make_shared<int[]>(10);
arr[0] = 42;

// unique_ptr数组
auto uarr = std::make_unique<Widget[]>(5);
  1. 原子共享指针(C++20)
cpp 复制代码
#include <atomic>
std::atomic<std::shared_ptr<Config>> globalConfig;

void updateConfig() {
    auto newConfig = std::make_shared<Config>();
    globalConfig.store(newConfig, std::memory_order_release);
}
  1. 内存映射文件管理
cpp 复制代码
auto mapFile = [](const std::string& path) {
    int fd = open(path.c_str(), O_RDONLY);
    void* addr = mmap(nullptr, fileSize, PROT_READ, MAP_PRIVATE, fd, 0);
    
    return std::shared_ptr<void>(addr, [fd](void* p) {
        munmap(p, fileSize);
        close(fd);
    });
};

五)性能关键场景优化

  1. 高频交易系统
cpp 复制代码
// 使用unique_ptr+裸指针访问
auto data = std::make_unique<Data>();
Data* rawPtr = data.get(); // 高频操作使用裸指针

// 析构时安全释放
data.reset(); // 低频操作
  1. 实时控制循环
cpp 复制代码
// 预分配对象池
std::vector<std::unique_ptr<SensorData>> pool;

// 循环内快速获取/释放
auto getData() {
    if(pool.empty()) {
        return std::make_unique<SensorData>();
    }
    auto ptr = std::move(pool.back());
    pool.pop_back();
    return ptr;
}

void releaseData(std::unique_ptr<SensorData> data) {
    data->reset();
    pool.push_back(std::move(data));
}

六)最佳实践总结

  1. 所有权策略选择
  • 独占所有权 → std::unique_ptr
  • 共享所有权 → std::shared_ptr
  • 观察访问 → std::weak_ptr
  1. 创建准则
  • 优先使用 make_unique/make_shared
  • 避免显式 newdelete
  • 工厂函数返回智能指针
  1. API设计原则
cpp 复制代码
// 良好设计示例
class ResourceManager {
public:
    // 返回独占指针(明确所有权转移)
    std::unique_ptr<Resource> acquireResource();
    
    // 接收共享指针(共享所有权)
    void cacheResource(std::shared_ptr<Resource> res);
    
    // 接收观察指针(不获取所有权)
    void registerObserver(std::weak_ptr<Observer> obs);
};
  1. 调试与检测
  • 使用 use_count() 检测共享引用
  • Valgrind/AddressSanitizer 检测内存问题
  • 静态分析工具检查所有权问题
  1. 异常安全保证
cpp 复制代码
void safeOperation() {
    auto res1 = std::make_unique<Resource>();
    auto res2 = std::make_unique<Resource>(); // 异常时自动释放
    
    // 可能抛出异常的操作
    processResources(res1.get(), res2.get());
    
    // 明确提交所有权
    commitResource(std::move(res1));
}

七)陷阱规避指南

  1. 避免循环引用 在树形/图结构使用 weak_ptr 断开循环

  2. 禁止裸指针二次封装

cpp 复制代码
Data* raw = new Data;
std::shared_ptr<Data> p1(raw);
// std::shared_ptr<Data> p2(raw); // 致命错误!
  1. 谨慎使用 get()
cpp 复制代码
auto ptr = std::make_shared<Data>();
Data* raw = ptr.get();
{
    // 错误:独立控制块导致双重释放
    std::shared_ptr<Data> bad(raw);
} // 此处释放内存!
// ptr 成为悬空指针
  1. 多线程安全
cpp 复制代码
// 共享指针本身线程安全,但数据访问需要同步
std::shared_ptr<Config> config;
std::mutex configMutex;

void update() {
    auto newConfig = std::make_shared<Config>();
    {
        std::lock_guard lock(configMutex);
        config = newConfig; // 原子赋值
    }
}

智能指针的正确使用能显著提升代码安全性和可维护性。掌握这些场景和技巧,可以让你:

  • 减少90%内存管理错误
  • 提高资源管理效率
  • 设计更清晰的API接口
  • 构建异常安全的代码
  • 实现高效的多线程资源共享

欢迎关注 【智践行】, 一起学习机器人开发,发送【C++】获得学习资料。

相关推荐
NuyoahC4 分钟前
笔试——Day43
c++·算法·笔试
彷徨而立1 小时前
【C++】 using声明 与 using指示
开发语言·c++
一只鲲1 小时前
48 C++ STL模板库17-容器9-关联容器-映射(map)多重映射(multimap)
开发语言·c++
智践行2 小时前
C++11之后的 Lambda 表达式 以及 `std::function`和`std::bind`
c++
智践行2 小时前
C++11移动语义‘偷梁换柱’实战
c++
祁同伟.3 小时前
【C++】模版(初阶)
c++
sTone873754 小时前
android studio之外使用NDK编译生成android指定架构的动态库
android·c++
卷卷卷土重来5 小时前
C++单例模式
javascript·c++·单例模式
yuyanjingtao5 小时前
CCF-GESP 等级考试 2025年6月认证C++二级真题解析
c++·青少年编程·gesp·csp-j/s