1. unique_ptr vs shared_ptr vs weak_ptr
1.1 三句话说清
| 类型 | 一句话 |
|---|---|
unique_ptr |
我独占,你不能拷贝,只能移交所有权 |
shared_ptr |
大家共享,最后一个释放的负责 delete |
weak_ptr |
我只是看看,不参与计数,不阻止释放 |
1.2 所有权模型图解
ini
unique_ptr:
ptr ───────▶ [资源]
独占,ptr 析构时资源释放
shared_ptr:
sp1 ──┐
sp2 ──┼──▶ [资源] (ref_count = 2)
sp3 ──┘
共享,ref_count 归零时资源释放
weak_ptr:
wp ─ ─ ─ ▶ [资源] (ref_count 不变)
弱引用,不阻止释放,访问需 lock()
1.3 选择原则
arduino
需要动态分配 → 先用 unique_ptr
需要多个持有者 → 升级为 shared_ptr
需要打破循环 → 其中一个改为 weak_ptr
2. shared_ptr 陷阱
2.1 同一裸指针初始化两个 shared_ptr
arduino
int *raw = new int(10);
shared_ptr<int> sp1(raw); // ref_count = 1
shared_ptr<int> sp2(raw); // ref_count = 1(另一个控制块!)
// sp1 析构 → delete raw ✅
// sp2 析构 → delete raw ❌ double free!
每个 shared_ptr 从裸指针构造时,会创建独立的控制块。 两个控制块各记各的,都以为自己是唯一的持有者。
正确做法:
arduino
auto sp1 = make_shared<int>(10); // 首选
shared_ptr<int> sp2 = sp1; // 拷贝,ref_count = 2
2.2 循环引用
css
class B; // 前置声明
class A {
public:
shared_ptr<B> bptr;
~A() { cout << "~A"; }
};
class B {
public:
shared_ptr<A> aptr; // ← 如果这是 weak_ptr 就能打破循环
~B() { cout << "~B"; }
};
{
auto a = make_shared<A>();
auto b = make_shared<B>();
a->bptr = b; // b 的 ref_count = 2
b->aptr = a; // a 的 ref_count = 2
} // a,b 出作用域,各自 ref_count 降到 1,但互指无法归零
// 析构函数永远不会被调用,内存泄漏!
css
循环引用图解:
a ──shared_ptr──▶ b
▲ │
└──shared_ptr────┘
ref_count(a) = 1(来自 b)
ref_count(b) = 1(来自 a)
无法释放
解决方案:
arduino
class B {
public:
weak_ptr<A> aptr; // ✅ 不增加引用计数
};
weak_ptr 使用方式:
scss
if (auto sp = wp.lock()) { // 提升为 shared_ptr
sp->do_something(); // 安全使用
} else {
// 资源已被释放
}
3. new/delete vs malloc/free
| new/delete | malloc/free | |
|---|---|---|
| 本质 | C++ 运算符 | C 库函数 |
| 调用构造函数 | ✅ new 时调用构造 | ❌ 只分配内存 |
| 调用析构函数 | ✅ delete 时调用析构 | ❌ 只释放内存 |
| 失败行为 | 抛 bad_alloc 异常 |
返回 NULL |
| 返回类型 | 具体类型指针 | void* 需强转 |
| 大小计算 | 编译器自动算 | 手动指定字节数 |
| 可重载 | ✅ | ❌ |
4. delete vs delete\[\]
arduino
int *p = new int[10];
delete p; // ❌ 未定义行为
delete[] p; // ✅ 正确
| delete | delete\[\] | |
|---|---|---|
| 配对 | new |
new[] |
| 析构调用 | 只调第一个 | 调所有元素 |
| 内置类型后果 | 可能不崩但未定义 | 正确 |
| 类类型后果 | 必定泄漏 | 正确 |
5. 移动语义
5.1 拷贝 vs 移动
makefile
拷贝: src → 复制数据 → dst (src 不变,成本高)
移动: src → 偷走指针 → dst (src 被掏空,成本极低)
5.2 std::move vs std::forward
| std::move | std::forward | |
|---|---|---|
| 作用 | 无条件转右值 | 条件转发 |
| 语义 | "我要移动" | "保持原样传下去" |
| 使用场景 | 已知要移动时 | 模板完美转发 |
| 实际上做了什么 | 强制类型转换 | 条件类型转换 |
typescript
template <typename T>
void relay(T &&arg) {
// forward:arg 是左值 → 转左值,arg 是右值 → 转右值
target(forward<T>(arg));
}
void target(string &s) { cout << "左值"; }
void target(string &&s) { cout << "右值"; }
5.3 移动构造函数的最大陷阱
scss
// ❌ 错误写法
Data(Data &&other) : ptr(other.ptr) {}
// 析构时 other 和 this 都指向同一内存 → double free
// ✅ 正确写法
Data(Data &&other) : ptr(other.ptr) {
other.ptr = nullptr; // 必须置空源对象指针 不然因为指针值复制,double free
}