C++ 智能指针与 RAII#

博客关于智能指针的再学习与理解有更完整的知识点与理解,建议优先阅读原文。

核心结论#

智能指针是 C++ 中用对象管理资源生命周期的一种方式。它把“资源的获取”和“资源的释放”绑定到对象的构造与析构上,从而减少手动 new / delete 带来的泄漏、重复释放和异常安全问题。

从使用角度看,可以先记住三句话:

  1. 默认优先使用 std::unique_ptr 表达唯一所有权。
  2. 只有确实需要共享生命周期时,才使用 std::shared_ptr
  3. 需要观察共享资源、但不参与所有权时,使用 std::weak_ptr

智能指针#

智能指针是一类用于管理动态资源的对象。它们封装了裸指针,并通过析构函数在合适的时机释放资源。

它不是传统意义上的 GC。智能指针更接近 RAII:当对象离开作用域、引用计数归零,或所有权被转移后,资源会按照 C++ 对象生命周期规则被释放。

类型对比#

类型所有权语义是否可复制常见场景注意点
std::unique_ptr独占所有权不可复制,可以移动工厂返回值、类成员、明确唯一拥有者优先用 std::make_unique 创建
std::shared_ptr共享所有权可以复制多个对象共同持有同一资源注意循环引用和引用计数开销
std::weak_ptr非拥有观察可以复制观察 shared_ptr 管理的对象、打破循环引用使用前需要 lock() 判断资源是否仍存在

std::unique_ptr#

std::unique_ptr 表示独占所有权。同一时刻只能有一个 unique_ptr 管理某个对象,因此它不能复制,只能移动。

C++
#include <memory>

std::unique_ptr<int> createNumber() {
    return std::make_unique<int>(5);
}

int main() {
    std::unique_ptr<int> ptr = createNumber();
    std::unique_ptr<int> movedPtr = std::move(ptr);

    // 此时 ptr 不再拥有资源,movedPtr 拥有资源。
    return 0;
}

适合使用 std::unique_ptr 的场景:

  • 资源只有一个明确拥有者。
  • 对象生命周期跟随某个类或函数作用域。
  • 工厂函数需要返回动态创建的对象。

std::shared_ptr#

std::shared_ptr 表示共享所有权。它通过引用计数记录当前有多少个 shared_ptr 持有同一资源,当引用计数归零时,资源会被自动释放。

C++
#include <memory>

int main() {
    auto ptr1 = std::make_shared<int>(5);
    auto ptr2 = ptr1;

    // ptr1 和 ptr2 共享同一个 int 对象。
    return 0;
}

适合使用 std::shared_ptr 的场景:

  • 资源生命周期确实需要被多个对象共同延长。
  • 很难明确指定唯一拥有者。
  • 对象需要跨异步任务、回调或复杂模块边界共享。

std::weak_ptr#

std::weak_ptr 通常与 std::shared_ptr 搭配使用。它不会增加引用计数,因此不会延长资源生命周期,适合用来观察共享资源。

C++
#include <iostream>
#include <memory>

int main() {
    auto ptr = std::make_shared<int>(5);
    std::weak_ptr<int> weakPtr = ptr;

    if (auto locked = weakPtr.lock()) {
        std::cout << *locked << '\n';
    }

    return 0;
}

std::weak_ptr 的典型价值是打破循环引用:

C++
#include <memory>

struct Node {
    std::shared_ptr<Node> next;
    std::weak_ptr<Node> prev;
};

RAII#

RAII 是 Resource Acquisition Is Initialization 的缩写,意思是“资源获取即初始化”。它的核心思想是:资源由对象管理,对象构造时获取资源,对象析构时释放资源。

RAII 能带来两个直接好处:

  • 自动释放资源:资源生命周期跟随对象生命周期,不需要手动在每条路径上释放。
  • 异常安全:即使中途抛出异常,已经构造完成的对象也会自动析构。

RAII 示例#

下面的例子用一个简单类演示 RAII 的基本结构:

C++
#include <iostream>

class Resource {
public:
    Resource() {
        data = new int[100];
        std::cout << "Resource acquired." << std::endl;
    }

    ~Resource() {
        delete[] data;
        std::cout << "Resource released." << std::endl;
    }

private:
    int* data;
};

void run() {
    Resource resource;
    // 在这里使用资源。
}

int main() {
    run();
    return 0;
}

实际项目中,通常更推荐直接使用标准库容器或智能指针表达 RAII,例如 std::vector<int>std::unique_ptr<T>std::shared_ptr<T>

智能指针与 RAII 的关系#

智能指针是 RAII 的一种典型实现。它们在构造时接管资源,并在析构时释放资源:

  • std::unique_ptr 在自身析构时释放独占资源。
  • std::shared_ptr 在最后一个共享拥有者析构或重置时释放资源。
  • std::weak_ptr 不拥有资源,只负责安全观察。

因此,智能指针解决的不是“如何像裸指针一样指向对象”,而是“谁负责释放对象,以及什么时候释放”。

使用建议#

  • 优先使用 std::make_uniquestd::make_shared 创建智能指针。
  • 不要把裸指针随意交给多个智能指针管理,否则容易重复释放。
  • 不要为了省事滥用 std::shared_ptr,共享所有权会让生命周期更难推理。
  • 遇到双向关系时,通常一侧使用 std::shared_ptr,另一侧使用 std::weak_ptr
  • 能用栈对象、标准库容器或引用表达的问题,不一定需要智能指针。

个人理解#

智能指针更像是面向开发者的“所有权指针”:它不仅指向内容,还明确表达资源归谁管理、什么时候释放。

裸指针则更偏底层,适合表达非拥有观察或底层接口。它和智能指针的关系,有点像基础数组和 std::vector 的关系:前者更原始,后者更适合日常业务开发。