DAY 5-智能指针与 C++ 内存管理

69 阅读3分钟

1. unique_ptr vs shared_ptr vs weak_ptr

1.1 三句话说清

类型一句话
unique_ptr我独占,你不能拷贝,只能移交所有权
shared_ptr大家共享,最后一个释放的负责 delete
weak_ptr我只是看看,不参与计数,不阻止释放

1.2 所有权模型图解

unique_ptr:
  ptr ───────▶ [资源]
  独占,ptr 析构时资源释放

shared_ptr:
  sp1 ──┐
  sp2 ──┼──▶ [资源] (ref_count = 2)
  sp3 ──┘
  共享,ref_count 归零时资源释放

weak_ptr:
  wp ─ ─ ─ ▶ [资源] (ref_count 不变)
  弱引用,不阻止释放,访问需 lock()

1.3 选择原则

需要动态分配 → 先用 unique_ptr
需要多个持有者 → 升级为 shared_ptr
需要打破循环 → 其中一个改为 weak_ptr

2. shared_ptr 陷阱

2.1 同一裸指针初始化两个 shared_ptr

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 从裸指针构造时,会创建独立的控制块。  两个控制块各记各的,都以为自己是唯一的持有者。

正确做法:

auto sp1 = make_shared<int>(10);  // 首选
shared_ptr<int> sp2 = sp1;        // 拷贝,ref_count = 2

2.2 循环引用

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,但互指无法归零
   // 析构函数永远不会被调用,内存泄漏!
循环引用图解:
  a ──shared_ptr──▶ b
  ▲                │
  └──shared_ptr────┘
  ref_count(a) = 1(来自 b)
  ref_count(b) = 1(来自 a)
  无法释放

解决方案:

class B {
public:
    weak_ptr<A> aptr;  // ✅ 不增加引用计数
};

weak_ptr 使用方式:

if (auto sp = wp.lock()) {   // 提升为 shared_ptr
    sp->do_something();       // 安全使用
} else {
    // 资源已被释放
}

3. new/delete vs malloc/free

new/deletemalloc/free
本质C++ 运算符C 库函数
调用构造函数✅ new 时调用构造❌ 只分配内存
调用析构函数✅ delete 时调用析构❌ 只释放内存
失败行为抛 bad_alloc 异常返回 NULL
返回类型具体类型指针void* 需强转
大小计算编译器自动算手动指定字节数
可重载✅❌

4. delete vs delete[]

int *p = new int[10];
delete p;    // ❌ 未定义行为
delete[] p;  // ✅ 正确
deletedelete[]
配对newnew[]
析构调用只调第一个调所有元素
内置类型后果可能不崩但未定义正确
类类型后果必定泄漏正确

5. 移动语义

5.1 拷贝 vs 移动

拷贝:  src → 复制数据 → dst    (src 不变,成本高)
移动:  src → 偷走指针 → dst    (src 被掏空,成本极低)

5.2 std::move vs std::forward

std::movestd::forward
作用无条件转右值条件转发
语义"我要移动""保持原样传下去"
使用场景已知要移动时模板完美转发
实际上做了什么强制类型转换条件类型转换
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 移动构造函数的最大陷阱

// ❌ 错误写法
Data(Data &&other) : ptr(other.ptr) {}
// 析构时 other 和 this 都指向同一内存 → double free

// ✅ 正确写法
Data(Data &&other) : ptr(other.ptr) {
    other.ptr = nullptr;  // 必须置空源对象指针 不然因为指针值复制,double free
}