C++ 智能指针

C++ 智能指针

十二月 13, 2025 次阅读

什么是智能指针

在 C++ 中,智能指针(Smart Pointer)本质上是一个对象,它的行为看起来像指针,但额外封装了资源管理语义,最核心的目标只有一个:

自动管理动态资源的生命周期,避免手动 new / delete 带来的错误。


为什么需要智能指针

在传统 C++ 中,动态内存通常这样管理:

int* p = new int(10);
// ...
delete p;

这种写法存在几个长期困扰 C++ 程序员的问题:

  • ❌ 忘记 delete → 内存泄漏
  • ❌ 重复 delete → 未定义行为
  • ❌ 异常路径提前返回 → 资源无法释放
  • ❌ 所有权不清晰 → 谁负责释放内存不明确

这些问题的本质原因是:

资源的申请和释放是“人为约定”的,而不是“语言强制”的。


智能指针解决的核心问题

智能指针引入了一个非常重要的思想:

让资源的生命周期与对象的生命周期绑定。

也就是说:

  • 资源在 构造函数中获取
  • 资源在 析构函数中释放

这正是 C++ 中经典的 RAII(Resource Acquisition Is Initialization)思想

示意代码:

{
    std::unique_ptr<int> p(new int(10));
    // 使用 p
} // 作用域结束,p 析构,内存自动释放

不需要显式 delete,也不怕异常提前离开作用域。


为什么“叫指针”,但本质是对象

虽然名字叫“智能指针”,但它并不是裸指针

std::unique_ptr<int> p;
  • p 是一个 对象
  • 内部 持有一个原始指针
  • 通过重载 *-> 等运算符,表现得“像指针一样使用”
*p = 42;
p->some_function();

所以可以这样理解:

智能指针 = 原始指针 + 析构策略 + 所有权语义


智能指针和普通指针的本质区别

对比点 原始指针 智能指针
是否自动释放
是否表达所有权
是否安全 相对安全
是否支持 RAII
是否可作为对象传递 ⚠️

智能指针并不是“万能的指针”

需要强调的是:

智能指针解决的是“资源管理问题”,不是“所有指针问题”。

它并不会:

  • 自动解决循环引用(shared_ptr 仍然可能泄漏)
  • 自动让程序“没有 bug”
  • 替代所有原始指针的使用场景

智能指针的价值在于:

把“释放资源”从程序员的责任,转移到语言和类型系统中。


  • 智能指针是 封装了资源管理语义的对象
  • 它通过 RAII 自动管理资源生命周期
  • 解决了手动 new / delete 带来的大量安全隐患
  • 是现代 C++ 资源管理的基础工具之一

unique_ptr —— 独占所有权的智能指针

std::unique_ptr 是 C++11 引入的智能指针,用来表达一种非常明确的语义:

某个资源在任意时刻只能被一个对象拥有。

这也是它名字中 unique 的真正含义。


unique_ptr 的核心语义:独占所有权

std::unique_ptr<int> p(new int(10));

此时:

  • p 唯一拥有这块动态内存
  • 不存在第二个 unique_ptr 可以同时管理同一资源
  • p 生命周期结束时,资源自动释放

这解决了裸指针中最难处理的问题之一:

“到底谁负责 delete?”

unique_ptr 中,答案是:类型本身明确表达了所有权


为什么 unique_ptr 不允许拷贝

下面的代码是不允许的

std::unique_ptr<int> p1(new int(10));
std::unique_ptr<int> p2 = p1;   // ❌ 编译错误

原因不是技术限制,而是语义约束

  • 拷贝意味着 两个对象同时拥有同一资源
  • 这直接违背了 “unique” 的设计初衷

所以 unique_ptr

  • ❌ 禁止拷贝构造
  • ❌ 禁止拷贝赋值

在底层设计上,实现的伪代码如下:

std::unique_ptr(const std::unique_ptr&) = delete;
std::unique_ptr& operator=(const std::unique_ptr&) = delete;

这样一来,就从类型系统层面杜绝“双重释放”的可能。


unique_ptr 是“可移动的”

虽然不能拷贝,但 unique_ptr 可以移动

std::unique_ptr<int> p1(new int(10));
std::unique_ptr<int> p2 = std::move(p1);

移动后:

  • p2 接管资源的所有权
  • p1 被置为空(内部指针为 nullptr

这里的语义是:

所有权发生转移,而不是共享。

这非常符合现实中的“交接责任”模型。


unique_ptr 与 RAII 的结合

unique_ptr 的析构函数中会自动释放资源:

{
    std::unique_ptr<int> p(new int(10));
} // 离开作用域,自动 delete

无论是:

  • 正常路径
  • return
  • 抛异常

资源都会被正确释放。

这正是 RAII 在现代 C++ 中的标准实践


推荐使用 make_unique

相比下面的写法:

std::unique_ptr<int> p(new int(10));

更推荐:

auto p = std::make_unique<int>(10);

原因有三点:

  1. 异常安全

    • 避免构造参数抛异常导致资源泄漏
  2. 代码更简洁

  3. 表达意图更清晰

在现代 C++ 中:

“不直接写 new”几乎是一个共识。


unique_ptr 的使用场景

unique_ptr 非常适合以下情况:

  • 管理堆对象的唯一所有权
  • 函数返回动态资源
  • 表达“拥有即负责释放”的语义
  • 作为类的成员管理资源

例如:

std::unique_ptr<Foo> createFoo() {
    return std::make_unique<Foo>();
}

unique_ptr 并不等于“不能传递”

虽然不能拷贝,但你可以:

  • 按引用传递(不转移所有权)
  • 按值 + std::move 传递(转移所有权)
void take(std::unique_ptr<Foo> p);        // 接管所有权
void observe(const std::unique_ptr<Foo>& p); // 只观察

类型本身就清楚表达了函数的意图。


  • unique_ptr 表达 独占所有权
  • 禁止拷贝,允许移动
  • 生命周期结束时自动释放资源
  • 是现代 C++ 中 首选的资源管理工具
  • 当你“只有一个拥有者”时,应优先使用 unique_ptr

shared_ptr —— 基于引用计数的共享所有权

std::shared_ptr 用来解决这样一类问题:

同一份资源需要被多个对象共同拥有,且生命周期无法由某一个对象单独决定。

unique_ptr 的“唯一所有权”不同,shared_ptr 表达的是 共享所有权


shared_ptr 的核心语义:共享所有权

auto p1 = std::make_shared<int>(10);
auto p2 = p1;

此时:

  • p1p2 共同管理同一块内存
  • 资源不会因为某一个指针离开作用域而被释放
  • 只有当 最后一个 shared_ptr 被销毁 时,资源才会释放

shared_ptr 的底层结构(key)

shared_ptr 并不只是一根指针,它至少包含两个部分:

  1. 被管理的对象指针
  2. 控制块(control block)

可以抽象成:

shared_ptr
 ├── ptr  ───────▶ 实际对象
 └── ctrl ───────▶ 控制块

控制块里有什么(引用计数核心)

控制块通常包含:

struct ControlBlock {
    atomic<size_t> shared_count; // 强引用计数
    atomic<size_t> weak_count;   // 弱引用计数
    Deleter deleter;             // 删除策略
};

其中最关键的是:

  • shared_count
    记录当前 有多少个 shared_ptr 在共享资源
  • weak_count
    用于 weak_ptr,后面章节展开

引用计数是如何工作的

创建时

auto p = std::make_shared<int>(10);
  • 分配一块内存(对象 + 控制块)
  • shared_count = 1

拷贝时

auto p2 = p;
  • shared_count++

析构时

p2.~shared_ptr();
  • shared_count--

  • 如果 shared_count == 0

    • 调用 deleter 释放对象
    • 控制块不一定立刻释放(weak_ptr 仍可能存在)

控制块的最终释放

  • 当:

    • shared_count == 0
    • weak_count == 0
  • 控制块才会被释放


为什么 shared_ptr 是线程安全的(重点)

shared_ptr 的“线程安全”指的是什么

标准保证的是:

不同的 shared_ptr 实例,在多线程中同时进行拷贝、析构是安全的。

也就是说:

shared_ptr<int> p = ...;

std::thread t1([p]{ /* 使用 p */ });
std::thread t2([p]{ /* 使用 p */ });

是安全的。


线程安全的实现原理

引用计数必须是原子的。

所以:

shared_count++;
shared_count--;

在标准实现中:

  • 使用 std::atomic
  • 或平台提供的原子指令(如 lock xadd

这是 shared_ptr 性能成本的重要来源


一个非常重要但常被误解的点

shared_ptr 只保证“控制块”的线程安全,不保证“对象本身”的线程安全。

例如:

shared_ptr<Foo> p;

p->modify();   // ❌ 如果多个线程同时调用,仍然是数据竞争

如果对象本身需要并发访问:

  • 仍然需要 mutex
  • 或对象本身是线程安全的

make_shared 的内存优势

auto p = std::make_shared<T>(...);

相比:

shared_ptr<T> p(new T(...));

优势在于:

  • 对象 + 控制块 一次分配
  • 更少的内存碎片
  • 更好的 cache locality

但代价是:

控制块和对象绑定,析构顺序不可分离

(比如自定义 deleter 场景)


shared_ptr 的典型问题:循环引用(埋个伏笔)

struct A {
    shared_ptr<B> b;
};

struct B {
    shared_ptr<A> a;
};
  • ABshared_count 永远不会归零
    • A:我要释放自己,但 B 还在引用我
    • B:我要释放自己,但 A 还在引用我
    • 很好,都别想释放了
  • 资源泄漏

这里的演示可能抽象了点,我们来说具体一点:

  • A 需要释放自己的时候,它会检查自己的 shared_count
    • 如果 shared_count 不为零,说明还有其他 shared_ptr 在引用它,它就不能释放自己。
  • 同理,B 也是一样的逻辑。

所以说,最后并不是说二者会互相等到导致卡住,这不是线程死锁问题,只是他们各自都不会释放自己,因为 shared_count 永远不会归零。所以才会导致资源泄漏。

👉 这正是 weak_ptr 存在的原因
(可以作为你下一节的自然过渡)


什么时候该用 shared_ptr

适合场景:

  • 多个对象共同拥有资源
  • 生命周期难以静态确定
  • 需要跨线程共享所有权

不适合场景:

  • 明确单一所有者
  • 性能极端敏感的代码
  • 能用 unique_ptr 解决的地方

如果能用 unique_ptr,就不要用 shared_ptr。


  • shared_ptr 基于 引用计数 管理资源
  • 控制块记录共享和弱引用数量
  • 引用计数操作是原子的,因此有性能成本
  • 线程安全只覆盖“引用计数”,不覆盖“对象本身”
  • 循环引用需要 weak_ptr 解决

weak_ptr —— 不参与所有权的观察者指针

std::weak_ptr 本质上是:

对 shared_ptr 所管理对象的一种“非拥有性引用”

它解决的是 shared_ptr 的一个根本问题:

循环引用导致引用计数永远无法归零


为什么需要 weak_ptr(动机)

先回顾 shared_ptr 的问题:

struct A {
    std::shared_ptr<B> b;
};

struct B {
    std::shared_ptr<A> a;
};

此时:

  • A 持有 B
  • B 持有 A
  • 两者 shared_count >= 1
  • 析构永远不会发生

根本原因是:

shared_ptr 之间形成了“强引用环”


weak_ptr 的语义本质

weak_ptr 的语义可以总结为一句话:

我能知道对象还活着,但我不决定它是否该死。

关键点:

  • ❌ 不增加 shared_count
  • ✅ 增加 weak_count
  • ❌ 不能直接解引用
  • ✅ 可以“尝试”获得一个 shared_ptr

weak_ptr 的底层结构(和 shared_ptr 的关系)

一个非常重要的事实:

weak_ptr 和 shared_ptr 指向的是同一个控制块

          ┌────────────┐
shared_ptr│  控制块     │◀──────── weak_ptr
          │────────────│
          │ shared_cnt │
          │ weak_cnt   │
          └────────────┘
                 │
                 ▼
             实际对象

weak_ptr 不保存对象指针本身,它只保存:

  • 控制块指针

weak_ptr 的核心数据:weak_count

控制块中:

struct ControlBlock {
    atomic<size_t> shared_count;
    atomic<size_t> weak_count;
};

创建 weak_ptr

std::shared_ptr<int> sp = ...;
std::weak_ptr<int> wp = sp;

发生的事情:

  • shared_count 不变
  • weak_count++

对象销毁 vs 控制块销毁(重点)

这是 weak_ptr 能存在的根本原因。

shared_count 归零时

shared_count == 0
  • ✅ 对象被销毁
  • ❌ 控制块仍然存在(只要 weak_count > 0

weak_count 也归零时

shared_count == 0 && weak_count == 0
  • 控制块才被释放

👉 这就保证了:

weak_ptr 可以在对象已被销毁后,安全地“观察”其状态


为什么 weak_ptr 不能直接解引用

std::weak_ptr<int> wp;
*wp;        // ❌ 不允许
wp->foo();  // ❌ 不允许

原因非常根本:

weak_ptr 不保证对象存在

如果允许解引用:

  • 可能访问已经析构的对象
  • 会直接导致悬垂引用

lock():weak_ptr 的核心操作

使用方式

if (auto sp = wp.lock()) {
    // 对象仍然存在
    sp->do_something();
} else {
    // 对象已经被销毁
}

lock() 的底层原理

lock() 做了三件事:

  1. 原子地检查 shared_count

  2. 如果 shared_count > 0

    • shared_count++
    • 构造一个新的 shared_ptr
  3. 否则返回空的 shared_ptr

关键点:这一步是原子的

防止:

  • 一个线程判断对象“还活着”
  • 另一个线程刚好析构最后一个 shared_ptr

expired() 的作用与局限

if (wp.expired()) { ... }

底层:

return shared_count == 0;

但要注意:

expired() 只能用于“快速判断”,不能作为逻辑保证

错误示例:

if (!wp.expired()) {
    auto sp = wp.lock(); // ❗ 仍可能失败(并发)
}

正确用法:

if (auto sp = wp.lock()) {
    ...
}

weak_ptr 解决循环引用的标准方式

struct A {
    std::shared_ptr<B> b;
};

struct B {
    std::weak_ptr<A> a;   // 断环
};

现在:

  • A → 强引用 → B
  • B → 弱引用 → A

A 释放:

  • A.shared_count == 0
  • B 的 weak_ptr 不阻止析构
  • 整个结构可正确释放

weak_ptr 的线程安全性

和 shared_ptr 一致:

  • weak_count 是原子操作
  • lock() 是线程安全的

但同样注意:

weak_ptr / shared_ptr 的线程安全 ≠ 对象的线程安全


典型使用场景

观察者模式

class Observer {
    std::weak_ptr<Subject> subject;
};

避免被观察者和观察者互相“绑死”。


缓存系统

std::unordered_map<Key, std::weak_ptr<Value>> cache;
  • 对象无人使用时自动回收
  • 缓存不会强行延长生命周期

回调 / lambda 捕获

std::weak_ptr<Foo> wp = sp;

callback = [wp] {
    if (auto sp = wp.lock()) {
        sp->do();
    }
};

避免回调延长对象生命周期。


  • weak_ptr 不拥有对象
  • 它只“观察” shared_ptr 的控制块
  • 通过 lock() 原子地尝试获取所有权
  • 它的存在是为了解决 shared_ptr 的结构性缺陷

shared_ptr 管生,weak_ptr 看死。
在类之间存在相互调用关系时,如果双方都使用 shared_ptr 持有对方,很容易形成循环引用,导致资源无法释放。

此时引入 weak_ptr,并不是为了“更细粒度地管理资源”,而是为了打破生命周期之间的强耦合

weak_ptr 本身并不参与对象的生命周期管理,它只作为一种“观察者”,用于判断对象是否仍然存在。在真正需要访问对象时,通过 lock() 临时获取一个 shared_ptr,在这一小段作用域内安全地使用对象。

可以将 weak_ptr 理解为一个“看门狗”:
它不会阻止对象被释放,但可以在进入前确认对象是否仍然存活。

正因为 weak_ptr 不拥有对象,所以它通常只应在以下场景中使用:
—— 对象之间存在依赖关系,但不应形成所有权关系。

如果一个对象本身就承担着资源所有权的职责,则仍然应该使用 shared_ptrunique_ptr,而不是直接依赖 weak_ptr