C++ 现代之路 (二):智能指针与资源革命——RAII 及现代智能指针深度剖析

「真正把 C++ 从 C with Classes 带入现代语言殿堂的,不是 Lambda、不是概念、不是协程,而是 RAII 与智能指针。」
——Bjarne Stroustrup 在多个场合反复强调的“现代 C++ 的灵魂”

本讲对应《C++ 现代之路》系列第二讲,也是无数大厂 C++ 岗位面试必问的“生死线”话题:
只要你写过一个裸 new 却忘了 delete,面试官就能立刻把你 pass 掉。

一、为什么 C++ 以前那么容易内存泄漏?

void bad()
{
    Foo* p = new Foo();           // 动态分配
    do_something_that_may_throw();
    // ……中间 100 行代码
    delete p;                     // 这一行永远到不了
}                                 // ← 泄漏 + 异常不安全

C 时代靠程序员自觉,C++98/03 靠“尽量记得写 delete”。
结果就是:所有稍大一点的项目都活在 Valgrind、ASan 的红色海洋里。

二、RAII:Resource Acquisition Is Initialization

RAII 是现代 C++ 的宗教级信条,可概括为 16 个字:

资源获取即初始化
生命周期由栈决定
析构函数绝不失约

void good()
{
    std::vector<Foo> v;           // 栈上对象
    v.push_back(Foo());           // 资源获取
    do_something_that_may_throw();
}                                 // ← 离开作用域自动析构,绝不泄漏

RAII 消灭了 99% 的资源泄漏问题,剩下的 1% 交给智能指针。

三、C++11 三大智能指针:所有权模型的完备解

智能指针 所有权语义 拷贝 移动 典型场景
unique_ptr 独占所有权 × 资源转移、返回值、容器元素
shared_ptr 共享所有权 需要多处共享、复杂图结构
weak_ptr 无所有权观察 打破循环引用、缓存
3.1 std::unique_ptr —— “现代 new/delete”
std::unique_ptr<Foo> p1 = std::make_unique<Foo>(1, 2); // C++14 推荐
auto p2 = std::move(p1);      // 所有权转移,p1 变为空
// p1->use();                 // 编译期就过不了,绝对安全
  • 大小和裸指针一样(不牺牲性能)
  • 支持自定义删除器(最常用在跨 DLL、文件句柄、锁等场景)
auto deleter = [](FILE* f){ if(f) fclose(f); };
std::unique_ptr<FILE, decltype(deleter)> fp(fopen("1.txt","r"), deleter);
3.2 std::shared_ptr —— 引用计数的全貌
std::shared_ptr<Foo> p1 = std::make_shared<Foo>(); // 一次分配控制块+对象
std::shared_ptr<Foo> p2 = p1;                      // 引用计数 2
std::weak_ptr<Foo>   w  = p1;                      // 不增加计数

控制块(control block)内部结构(简化版):

struct _ControlBlock {
    std::atomic<long> strong_ref;   // 1
    std::atomic<long> weak_ref;     // +1 (weak_ptr)
    // deleter, allocator...
};

线程安全要点(面试必问):

  • 引用计数的增减是原子操作,线程安全
  • 控制块本身线程安全,但被管理对象本身不保证线程安全
  • 不同 shared_ptr 实例指向同一对象是线程安全的(增减计数安全)
3.3 std::weak_ptr —— 打破循环引用的唯一解

经典循环引用导致永不释放:

struct Node {
    std::shared_ptr<Node> next;
    // std::shared_ptr<Node> prev;  // 相互持有 → 泄漏!
    std::weak_ptr<Node> prev;       // 改成 weak_ptr
};

检查是否过期:
if (auto sp = w.lock()) {
sp->do_something();
}
};


### 四、make_unique / make_shared 为什么是强制要求?

```cpp
// 危险!可能泄漏
process(std::shared_ptr<Foo>(new Foo()), compute());

// 安全写法(C++14/17)
process(std::make_shared<Foo>(), compute());
process(std::make_unique<Foo>(), compute());

原因:异常安全。如果 compute() 抛异常,new Foo 的内存可能来不及交给 shared_ptr 接管。

make_shared 还有额外性能优势:一次分配控制块 + 对象,缓存命中率更高。

五、面试高频手撕题(现场必会)

  1. 实现一个简版 unique_ptr(支持移动、不支持自定义删除器)
template<typename T>
class MyUniquePtr {
    T* ptr = nullptr;
public:
    explicit MyUniquePtr(T* p = nullptr) : ptr(p) {}
    ~MyUniquePtr() { delete ptr; }

    // 禁用拷贝
    MyUniquePtr(const MyUniquePtr&) = delete;
    MyUniquePtr& operator=(const MyUniquePtr&) = delete;

    // 移动语义
    MyUniquePtr(MyUniquePtr&& other) noexcept : ptr(other.ptr) {
        other.ptr = nullptr;
    }
    MyUniquePtr& operator=(MyUniquePtr&& other) noexcept {
        if (this != &other) {
            delete ptr;
            ptr = other.ptr;
            other.ptr = nullptr;
        }
        return *this;
    }

    T* operator->() const { return ptr; }
    T& operator*()  const { return *ptr; }
    T* get()        const { return ptr; }
    explicit operator bool() const { return ptr != nullptr; }
};
  1. 实现一个线程安全的简版 shared_ptr(只看思路)
  • 用一个结构体包含 atomic strong, weak
  • 构造时 new 控制块
  • copy 时 atomic fetch_add
  • 析构时 fetch_sub,若 strong 变为 0 则 delete 对象,再判断 weak 是否为 0 来 delete 控制块

六、在 UE5 大型项目中的真实编码规范(2025 最新)

// 1. 永远禁止裸 new/delete(Clang-Tidy + 编译器插件直接 error)
Foo* p = new Foo();   // 编译不过
delete p;             // 编译不过

// 2. 返回值一律用 unique_ptr(所有权转移最清晰)
std::unique_ptr<AActor> SpawnMyActor() {
    return MakeUnique<AActor>();
}

// 3. 容器里存 unique_ptr(节省一次堆分配)
TArray<std::unique_ptr<FComponent>> Components;

// 4. 需要共享的才用 shared_ptr,且必须 make_shared
TMap<FName, TSharedPtr<FConfig>> ConfigCache;

// 5. 父子关系用 TWeakObjectPtr(UE 自带,自动置空)
TWeakObjectPtr<AActor> Parent;

七、本讲总结:一句话记住现代 C++

资源用栈管理,动态资源交给智能指针,永远不要再写 new/delete。

掌握了 RAII + 三大智能指针,你就跨过了现代 C++ 的第一道门槛,后面所有新特性(移动语义、完美转发、协程、Ranges……)都是在此基础上开出来的花。

下一讲《C++ 现代之路 (三)》我们将进入“右值引用与移动语义”的工业级深度剖析——真正让 std::vector 扩容零拷贝、让大对象传递零开销的幕后英雄。

敬请期待。

Logo

有“AI”的1024 = 2048,欢迎大家加入2048 AI社区

更多推荐