C++ 中资源管理的核心思想:利用对象生命周期自动释放资源,避免内存泄漏、双重释放等问题。 本部分涵盖:RAII、简易智能指针、auto_ptrunique_ptrshared_ptr 及循环引用、weak_ptr 原理与实现。


1. 传统 new/delete 的缺陷与 RAII 思想

int div()
{
    int a, b;
    cin >> a >> b;
    if (b == 0)
        throw invalid_argument("除0错误");
    return a / b;
}
​
void f()
{
    pair<string, string>* p1 = new pair<string, string>;
    pair<string, string>* p2 = new pair<string, string>;
    pair<string, string>* p3 = new pair<string, string>;
    pair<string, string>* p4 = new pair<string, string>;
    
    try
    {
        div();
    }
    catch (...)
    {
        delete p1;
        cout << "delete:" << p1 << endl;
        throw;
    }
    div();
    delete p1;
    //如果 div() 抛异常,catch 里面 delete p1 一次,重新抛出后 函数末尾的 delete p1 又执行一次 → 未定义行为(双重释放)
    //若div()正常,但p2p3p4无法得到释放,存在内存泄漏
}

问题场景:当异常抛出时,函数中手工 new 的资源很容易忘记释放,导致泄漏或双重释放。

new/delete的缺陷

1.必须在所有执行路径上都配对 new 和 delete

2.异常会打乱正常控制流

3.指针没有所有权语义,p1仅是一个地址,无法表达谁负责释放,也不能在离开作用域时自动销毁

4.可维护性差


2. 最简 RAII 智能指针

RAII(Resource Acquisition Is Initialization,资源获取即初始化) 是 C++ 中一种重要的资源管理编程范式,利用对象生命周期来控制程序资源

template<class T>
class SmartPtr
{
public:
    SmartPtr(T* ptr)
        :_ptr(ptr)
    {}
​
    ~SmartPtr()
    {
        cout << "delete:" << _ptr << endl;
        delete _ptr;
    }
​
    T& operator*()
    {
        return *_ptr;
    }
​
    T* operator->()
    {
        return _ptr;
    }
​
private:
    T* _ptr;
};
​
void f()
{
    pair<string, string>* p1 = new pair<string, string>;
    // ... 可能抛出异常的代码 div();
    SmartPtr sp1(p1);   // 资源托管给智能指针,离开作用域自动释放
    SmartPtr<pair<string, string>> sp2(new pair<string, string>("hello", "world"));
    SmartPtr<string> sp3(new string("hello world"));
​
    std::cout << *sp3 << endl;
    std::cout << sp2->first << " " << sp2->second << endl;
}
  • RAII(Resource Acquisition Is Initialization):资源获取即初始化,将资源(如堆内存)的生命周期绑定到对象的生命周期。对象构造时获取资源,析构时自动释放。

  • 上面的 SmartPtr 通过重载 operator*operator-> 模拟指针行为。

  • 缺陷:不支持拷贝(拷贝会导致同一指针被两次 delete)。


3. auto_ptr 的历史教训

template <typename T>
class AutoPtr
{
    T* ptr;
public:
    explicit AutoPtr(T* p = nullptr) : ptr(p) {}
​
    ~AutoPtr() { delete ptr; }
​
    // 拷贝构造:转移所有权(可怕的“假拷贝”)
    AutoPtr(AutoPtr& other) 
        : ptr(other.ptr) 
        { 
            other.ptr = nullptr; // 源指针被置空
        }
    
    // 拷贝赋值运算符:转移所有权
    AutoPtr& operator=(AutoPtr& other) 
    {
        if (this != &other) 
        {
            delete ptr;         // 释放自己原来的资源
            ptr = other.ptr;    // 接管对方的资源
            other.ptr = nullptr;// 源指针置空
        }
        return *this;
    }
​
    T& operator*() const { return *ptr; }
    T* operator->() const { return ptr; }
};
​
void func(AutoPtr<int> p) {}   // 按值传参会发生所有权转移
​
void test_2()
{
    AutoPtr<int> sp1(new int(5));
    AutoPtr<int> sp2 = sp1;   // 看似拷贝,实则sp1 被置空,sp2 接管资源
    // *sp1 = 10;             // 崩溃!sp1 已经是 nullptr
    func(sp2);                 // sp2所有权转移到参数 p,函数返回后 p 析构,资源释放
    // *sp2 = 11;             // 崩溃!sp2 悬空
    AutoPtr<int> arr(new int[10]); // 析构时调用 delete,而非 delete[],未定义行为
}
  • C++98 引入的 auto_ptr 试图用所有权转移解决拷贝问题,但其“拷贝即转移”的行为违反直觉,导致悬空指针和崩溃。

  • 不能用于 STL 容器,且不支持数组。

  • C++11 废弃 auto_ptr,引入独占所有权的 unique_ptr 和共享所有权的 shared_ptr


4. unique_ptr —— 独占所有权

unique_ptr禁止拷贝,shared_ptr支持拷贝构造

void test_3()
{
    unique_ptr<string> up1(new string("unique_ptr_1"));
    unique_ptr<string> up2(new string("unique_ptr_2"));
    cout << "up1:" << *up1 << endl;
    // unique_ptr<string> up3(up2);   // 错误:禁止拷贝
    unique_ptr<string> up3 = move(up1); // 显式移动,up1 变空
    cout << "up2:" << *up2 << " up3:" << *up3 << endl;
​
    shared_ptr<string> sp1(new string("shared_ptr_1"));
    shared_ptr<string> sp2(new string("shared_ptr_2"));
    shared_ptr<string> sp3(sp2);
    cout << "sp1:" << *sp1 << " sp2:" << *sp2 << " sp3:" << *sp3 << endl;
}

自定义实现(简化版)

template<class T>
class Unique_Ptr
{
public:
    Unique_Ptr(T* ptr) : _ptr(ptr) {}
​
    ~Unique_Ptr() { cout << "delete:" << _ptr << endl; delete _ptr; }
​
    T& operator*() { return *_ptr; }
    T* operator->() { return _ptr; }
​
    Unique_Ptr(Unique_Ptr<T>& ap) = delete;          // 禁止拷贝构造
    Unique_Ptr<T>& operator=(Unique_Ptr<T>& ap) = delete; // 禁止拷贝赋值
​
private:
    T* _ptr;
};
  • unique_ptr 独占资源所有权,不可拷贝只可移动,语义清晰安全。

  • 通过 = delete 明确禁止拷贝,同时支持移动构造/移动赋值(示例未实现移动,真实 unique_ptr 支持 move 转移所有权)。

  • 相比于 auto_ptr,所有权转移必须显式使用 std::move,不会在不知不觉中发生悬空。


5. shared_ptr 与引用计数

template<class T>
class Shared_Ptr
{
public:
    Shared_Ptr(T* ptr)
        :_ptr(ptr)
        ,_pcount(new int(1))
    {}
​
    ~Shared_Ptr()
    {
        if (--(*_pcount) == 0)
        {
            cout << "delete:" << _ptr << endl;
            delete _ptr;
            delete _pcount;
        }
    }
​
    explicit operator bool() const { return _ptr != nullptr; }
    T& operator*() { return *_ptr; }
    T* operator->() { return _ptr; }
​
    Shared_Ptr(const Shared_Ptr<T>& sp)
        :_ptr(sp._ptr), _pcount(sp._pcount)
    {
        ++(*_pcount);
    }
​
    Shared_Ptr<T>& operator=(const Shared_Ptr<T>& sp)
    {
        if (_ptr == sp._ptr) return *this;
        if (--(*_pcount) == 0)
        {
            delete _ptr;
            delete _pcount;
        }
        _ptr = sp._ptr;
        _pcount = sp._pcount;
        ++(*_pcount);
        return *this;
    }
​
    int use_count() const { return *_pcount; }
    T* get() const { return _ptr; }
    int* get_pcount() const { return _pcount; }
​
private:
    T* _ptr;
    int* _pcount; // 引用计数,所有共享者共享同一块计数内存
};
  • shared_ptr 通过引用计数实现共享所有权:每多一个对象共享,计数加一;析构或赋值释放时计数减一,减到 0 时释放资源。

  • 内部使用了堆上的 int 来存储计数,确保所有副本可见同一计数。

  • 拷贝构造和赋值操作需要正确处理计数的增减。

  • 提供了 use_count() 获取当前引用数,get() 获取裸指针。


6. weak_ptr 与循环引用

循环引用问题:当两个对象互相持有对方的 shared_ptr 时,引用计数永远不会归零,导致内存泄漏。

struct Node
{
    A _val;
    // Node* _next;    // 使用裸指针无法自动释放
    // Node* _prev;
    Weak_Ptr<Node> _next;   // 使用 weak_ptr 打破循环
    Weak_Ptr<Node> _prev;
};

自定义 Weak_Ptr(简化演示)

template<class T>
class Weak_Ptr
{
public:
    Weak_Ptr() : _ptr(nullptr) {}
​
    Weak_Ptr(const Shared_Ptr<T>& sp) : _ptr(sp.get()) {}
​
    Weak_Ptr<T>& operator=(const Shared_Ptr<T>& sp)
    {
        _ptr = sp.get();
        return *this;
    }
​
    T& operator*() { return *_ptr; }
    T* operator->() { return _ptr; }
​
private:
    T* _ptr;
};
  • weak_ptr 是一种弱引用,它指向由 shared_ptr 管理的对象,但不增加引用计数

  • 主要用途:解决 shared_ptr 的循环引用问题。比如双向链表中,节点间可以使用 shared_ptr 指向下一个,但指向前一个应使用 weak_ptr,否则形成环,导致永远不会释放。

  • 上述例子里,Weak_Ptr 仅保存裸指针,未与 Shared_Ptr 的引用计数联动(完美的 weak_ptr 需能检测资源是否已释放,标准库的 weak_ptr 可通过 lock() 返回一个有效的 shared_ptr 或空指针)。

  • 不参与资源释放管理weak_ptr 不会在析构时 delete 对象,也不阻止对象被销毁。


7. 综合测试:所有权转移与循环引用

class A
{
public:
    A(int a = 0) : _a(a) { cout << "A(int a = 0)" << endl; }
    ~A() { cout << this; cout << " ~A()" << endl; }
​
private:
    int _a;
};
​
void test_4()
{
    cout << "---------- Unique_Ptr独占所有权 ----------" << endl;
    Unique_Ptr<string> up1(new string("Unique_Ptr_1"));
    Unique_Ptr<string> up2(new string("Unique_Ptr_2"));
    cout << "UP1:" << *up1 << "  UP2:" << *up2 << endl;
    // up1 = up2; // ❌ 拷贝赋值被 delete
​
    cout << "\n--------- Shared_Ptr + Weak_Ptr共享与观察 ----------" << endl;
    Shared_Ptr<string> sp1(new string("Shared_Ptr_1"));
    cout << "SP1: " << *sp1 << "  use_count: " << sp1.use_count() << endl; // 1
​
    Weak_Ptr<string> wp1(sp1);          // 观察 sp1
    cout << "wp1: " << *wp1 << endl;
​
    Shared_Ptr<string> sp2(sp1);        // 共享,计数变 2
    cout << "SP2: " << *sp2 << "  use_count: " << sp1.use_count() << endl;
    Weak_Ptr<string> wp2;
    wp2 = sp2;
    cout << "wp2: " << *wp2 << endl;
​
    Shared_Ptr<string> sp3(new string("Shared_Ptr_2"));
    cout << "SP3: " << *sp3 << "  use_count: " << sp3.use_count() << endl;
​
    sp3 = sp1;  // sp3 释放原资源,改为指向 sp1 的资源,计数变 3
    cout << "after sp3 = sp1, SP3: " << *sp3 << "  use_count: " << sp1.use_count() << endl;
​
    // 离开内层作用域 sp2, sp3 析构,但 sp1 仍持有,资源不会被释放
}
  • 所有 Weak_Ptr 对象(wp1, wp2)仅观察资源,不影响 Shared_Ptr 的引用计数。

  • 当最后一个 Shared_Ptr 被销毁时,资源被释放。此时若再用 weak_ptr 访问则属于野指针,标准库的 weak_ptr 提供 expired() 判断和 lock() 安全访问。

  • 上述自定义 Weak_Ptr 缺乏安全检查,仅用于演示弱引用的概念。实际项目中请使用 std::weak_ptr


总结

  • RAII:资源绑定对象生命周期,自动释放。

  • unique_ptr:独占所有权,不可拷贝,可移动。

  • shared_ptr:共享所有权,引用计数,注意循环引用。

  • weak_ptr:弱引用,不增加计数,配合 shared_ptr 打破循环引用。

  • 现代 C++ 应尽量避免裸 new/delete,优先使用智能指针。

更多推荐