C++ STL 之智能指针详解:用法 × 原理 × 面试
三段式:先讲怎么用,再讲底层为什么这样设计,最后给口语化面试答案。
一、用法速查
1.1 unique_ptr —— 独占所有权
同一时刻只有一个 unique_ptr 指向某块资源。禁止拷贝,只支持移动。
#include <memory>
#include <iostream>
#include <vector>
using namespace std;
struct Widget {
int id;
Widget(int i) : id(i) { cout << "Widget(" << id << ")\n"; }
~Widget() { cout << "~Widget(" << id << ")\n"; }
};
int main() {
auto u1 = make_unique<Widget>(1);
u1.reset(new Widget(2));
Widget *raw = u1.get();
cout << raw->id << "\n";
Widget *leaked = u1.release();
delete leaked;
auto deleter = [](Widget *p) {
cout << "自定义删除器\n";
delete p;
};
unique_ptr<Widget, decltype(deleter)> u2(new Widget(3), deleter);
auto u3 = make_unique<Widget>(4);
auto u4 = move(u3);
vector<unique_ptr<Widget>> vec;
vec.push_back(make_unique<Widget>(5));
}
1.2 shared_ptr —— 共享所有权
引用计数共享所有权,最后一个 shared_ptr 销毁时释放资源。
#include <memory>
#include <iostream>
using namespace std;
struct Resource {
int val;
Resource(int v) : val(v) { cout << "Resource(" << v << ") created\n"; }
~Resource() { cout << "Resource(" << val << ") destroyed\n"; }
};
int main() {
auto sp1 = make_shared<Resource>(100);
auto sp2 = sp1;
cout << "use_count: " << sp1.use_count() << "\n";
cout << sp1->val << "\n";
cout << (*sp1).val << "\n";
sp2.reset();
sp1.reset(new Resource(200));
auto file_deleter = [](FILE *fp) {
cout << "关闭文件\n";
fclose(fp);
};
shared_ptr<FILE> sp_fp(fopen("test.txt", "w"), file_deleter);
shared_ptr<int> sp_arr(new int[10]{}, [](int *p) { delete[] p; });
}
shared_ptr 拷贝是 O(1),只有原子自增。
1.3 weak_ptr —— 弱引用 / 打破循环
不控制生命周期,通过 lock() 原子升级到 shared_ptr。
#include <memory>
#include <iostream>
using namespace std;
int main() {
auto sp = make_shared<int>(42);
weak_ptr<int> wp = sp;
cout << "use_count: " << sp.use_count() << "\n";
if (auto locked = wp.lock()) {
cout << "值: " << *locked << "\n";
} else {
cout << "对象已释放\n";
}
sp.reset();
auto locked2 = wp.lock();
if (!locked2) {
cout << "lock 返回空\n";
}
}
循环引用示例:
struct Child;
struct Parent {
vector<shared_ptr<Child>> children;
~Parent() { cout << "~Parent\n"; }
};
struct Child {
weak_ptr<Parent> parent;
~Child() { cout << "~Child\n"; }
};
int main() {
auto p = make_shared<Parent>();
auto c = make_shared<Child>();
p->children.push_back(c);
c->parent = p;
}
1.4 enable_shared_from_this —— 安全返回 this
继承 enable_shared_from_this<T>,调用 shared_from_this() 返回同一控制块的 shared_ptr,避免 double-free。
#include <memory>
#include <iostream>
#include <vector>
using namespace std;
struct Node : enable_shared_from_this<Node> {
int val;
vector<shared_ptr<Node>> neighbors;
Node(int v) : val(v) {}
void add_neighbor(shared_ptr<Node> n) {
neighbors.push_back(n);
}
shared_ptr<Node> shared_self() {
return shared_from_this();
}
};
int main() {
auto n1 = make_shared<Node>(1);
auto n2 = make_shared<Node>(2);
n2->add_neighbor(n1->shared_self());
cout << "Node1 use_count: " << n1.use_count() << "\n";
}
只有已被 shared_ptr 管理的对象才能调用 shared_from_this(),栈对象调用会抛出 bad_weak_ptr。
1.5 快速查表
| 操作 | unique_ptr | shared_ptr | weak_ptr |
|---|---|---|---|
| 创建 | make_unique<T>(args) | make_shared<T>(args) | wp = sp |
| 获取指针 | .get() | .get() | .lock() 升级 |
| 释放所有权 | .release() | — | — |
| 重置 | .reset() | .reset() | .reset() |
| 判空 | if (!up) | if (!sp) | .expired() |
| 引用计数 | — | .use_count() | .use_count() |
| 自定义删除器 | 支持(影响 sizeof) | 支持(不影响 sizeof) | — |
| 拷贝 | 禁止 | 引用计数 +1 | 引用计数不变 |
二、底层原理
2.1 unique_ptr 的结构——EBO 与零开销
unique_ptr 内部存储一个 T* 和一个删除器。当删除器是空类时,通过 EBO(空基类优化)让它不占空间。
template<typename T, typename Deleter = default_delete<T>>
class unique_ptr {
// tuple<T*, Deleter>
// Deleter 为空类 → EBO → 0 字节
};
sizeof(unique_ptr<T>) == sizeof(T*)(x64 下 8 字节)- 有状态删除器(函数指针、有捕获 lambda)会让
sizeof增大 unique_ptr<T[]>偏特化:析构调用delete[],支持operator[]
没有引用计数,没有控制块——零成本抽象。
2.2 shared_ptr 的控制块——双计数模型
shared_ptr 内部有两个指针:裸指针(指向对象)和控制块指针(指向堆上的 control block)。
控制块结构:
control_block(堆上分配)
┌──────────────────────────────────────────┐
│ strong ref count(强引用计数) │
│ weak ref count(弱引用计数) │
│ deleter(删除器,类型擦除后的函数对象) │
│ allocator(分配器) │
└──────────────────────────────────────────┘
- strong ref:指向对象的
shared_ptr数量,归零时销毁对象 - weak ref:指向控制块的
weak_ptr数量,strong + weak 都归零时释放控制块
2.3 make_shared 为什么更好
// 两次分配
shared_ptr<Widget> sp(new Widget(42));
// 分配1: new Widget(42)
// 分配2: new control_block
// 一次分配
auto sp2 = make_shared<Widget>(42);
// 一次 malloc: control_block + Widget 连续内存
优势:
- 减少一次堆分配
- 缓存友好——控制块和对象在同一 cache line
- 异常安全——无裸 new 中间状态
陷阱:生命周期延长。只要还有 weak_ptr 指向控制块,即使 strong ref 已归零、对象已析构,控制块所在的那一整块内存一直不得释放。大对象 + 长生命周期 weak_ptr 可能造成内存滞留。
2.4 weak_ptr 的工作原理
weak_ptr 和对应的 shared_ptr 共享同一控制块,但只增加 weak ref,不增加 strong ref。
// lock() 简化语义
shared_ptr<T> lock() noexcept {
if (控制块.strong_ref > 0) {
原子操作 ++strong_ref;
return shared_ptr<T>(裸指针, 控制块);
}
return nullptr;
}
lock() 是原子操作:读取 strong ref 和自增之间没有竞态。不需要先 expired() 再 lock()——不存在 TOCTOU 问题。
2.5 enable_shared_from_this 的原理
内部藏了一个 weak_ptr<T>。类第一次被 shared_ptr 管理时(如 make_shared),shared_ptr 构造函数检测到 T 继承自 enable_shared_from_this<T>,把内部的 weak_ptr 指向自己的控制块。
shared_from_this() 等价于 weak_ptr::lock()——从同一控制块安全返回 shared_ptr。
栈对象不曾被 shared_ptr 管理过,内部 weak_ptr 为空,lock() 返回空或抛出 bad_weak_ptr。
2.6 循环引用
struct A { shared_ptr<B> b; };
struct B { shared_ptr<A> a; };
auto a = make_shared<A>();
auto b = make_shared<B>();
a->b = b;
b->a = a;
// 双方引用计数都是 2 → 谁也释放不了谁
解法:把其中一方的 shared_ptr 改为 weak_ptr。对谁的生命周期不负责任,谁就持有 weak_ptr:
struct A { shared_ptr<B> b; };
struct B { weak_ptr<A> a; };
2.7 性能开销速查
| 操作 | 开销 | 说明 |
|---|---|---|
unique_ptr 构造/析构 | 0 | 等同于裸指针 new/delete |
shared_ptr 拷贝(原子自增) | ~25-40ns | CAS 原子操作 |
shared_ptr 析构(原子自减) | ~25-40ns | 归零时走虚函数删除器 |
weak_ptr::lock() | ~25-40ns + 分支 | 原子读 + 条件自增 |
make_shared vs 分开 new | 减少 ~50ns + 1 次 malloc | 控制块 + 对象合一块 |
shared_ptr 每次拷贝和销毁都涉及原子操作。高并发下大量拷贝 shared_ptr,CAS 缓存行 bouncing 的开销可能远超 40ns。
三、面试题 + 口语化答案
Q1:sizeof(unique_ptr<T>) 是多少?
“默认 8 字节(x64),和裸指针一样大。因为 default_delete 是空类,EBO 不占额外空间。有状态的删除器(存了函数指针或上下文)会让 sizeof 变大。”
Q2:make_shared 为什么比直接 new 好?有什么缺点?
“一次分配代替两次,控制块和对象合一块内存——减少 malloc、缓存友好、异常安全。缺点是控制块和对象绑在一起,只要 weak_ptr 活着,即使对象已析构,那块内存也不释放。大对象 + 长生命周期 weak_ptr 可能内存滞留。”
Q3:循环引用怎么解决?
“把其中一方的 shared_ptr 改成 weak_ptr。子持有父的 weak_ptr——子不控制父生命周期,需要时通过 lock() 临时升级成 shared_ptr。判断标准:一方对另一方的生命周期不负责任。”
Q4:weak_ptr::lock() 是原子的吗?
“是。内部原子读 strong ref,非零则原子自增并返回 shared_ptr;为零返回 nullptr。不需要先 expired() 再 lock(),不存在 TOCTOU 问题。”
Q5:自定义删除器影响 sizeof 吗?
“unique_ptr 会——默认删除器是空类(0 字节),有状态的删除器让 sizeof 变大。shared_ptr 不会——删除器存在控制块里,类型擦除后用虚函数调用,sizeof 始终两个指针(16 字节,x64)。”
Q6:enable_shared_from_this 为什么不能用在栈对象?
“因为 shared_from_this() 内部是 weak_ptr::lock()——需要从已有控制块返回 shared_ptr。栈对象没被 shared_ptr 管理过,控制块不存在,内部 weak_ptr 是空的,lock() 返回空或抛出 bad_weak_ptr。”
Q7:shared_ptr 的性能开销多大?
“每次拷贝构造/赋值内部是 CAS 原子操作,x86 下约 25-40ns。热路径上频繁拷贝 shared_ptr(如每秒百万次异步回调捕获)这个开销显著。推荐用移动构造(不涉及原子操作)或 unique_ptr + 借用。”
Q8:控制块在哪分配?weak_ptr 参与生命周期吗?
“控制块在堆上。strong ref 控制对象生命周期——归零时销毁对象。weak ref 控制控制块生命周期——strong + weak 都归零时控制块才释放。这就是为什么 weak_ptr 可以安全地用 lock() 去试:控制块始终有效。”
Q9:unique_ptr 可以用于数组吗?
“可以。unique_ptr<T[]> 是偏特化,析构调用 delete[],支持 operator[]。shared_ptr 默认是 delete,不支持数组——需手动传 default_delete<T[]> 或 lambda。C++17 后 shared_ptr<T[]> 也有了偏特化。”
Q10:make_unique 有异常安全优势吗?
“相比裸 new 确实不会因构造函数抛异常而泄漏,但现代代码很少裸 new 了。make_unique 的真正价值是语义清晰:一眼看出是独占指针。”
一句话总结:unique_ptr 零成本独占(sizeof = 裸指针),shared_ptr 双计数控制块共享(拷贝约 25-40ns 原子 CAS),weak_ptr 通过 weak ref 安全观测不延长生命周期;理解控制块结构、make_shared 一次分配的优势与生命周期延长副作用、enable_shared_from_this 的 weak_ptr 底层原理,是用好和面好智能指针的关键。
4293



被折叠的 条评论
为什么被折叠?



