RAII思想

RAII:资源获取即初始化—>把资源的生命周期绑定到栈上对象的生命周期,栈上对象离开作用域就自动销毁了

  • 对象构造,拿到资源
  • 对象析构,释放资源

资源的释放不再靠手动完成,而是根据类的特性自动完成

RAII管理的是资源,不仅仅是内存

常见的资源:uniqe_ptr

  • 堆内存:new/delete
  • 文件句柄:fopen/fclose
  • 互斥锁:lock/unlock
  • socket:创建/关闭

RAII可以保证异常安全

例如:

#include <iostream>
using namespace std;

class IntHolder {
private:
    int* ptr;
public:
    IntHolder(int value) : ptr(new int(value)) {
        cout << "构造申请资源\n";
    }
    ~IntHolder() {
        cout << "析构释放资源\n";
        delete ptr;
    }
};
void func() {
    IntHolder obj(100);
    throw runtime_error("出错");
}
int main() {
    try {
        func();
    } catch (const exception& e) {
        cout << "捕获到异常: " << e.what() << endl;
    }
    return 0;
}

obj 是局部对象,异常抛出后func() 在栈展开过程中退出作用域,局部对象会自动调用析构函数,因此 IntHolder 能在析构中释放资源,这就是 RAII 的异常安全性。

unique_ptr 独占型

unique_ptr是一种独占所有权的智能指针,同一时刻只能有一个unique_ptr拥有这个资源,对象析构自动释放资源—>我独有,不能拷贝,可移动,转移所有权

例如:unique_ptr<int> p1(new int(10));

这个intp1所有,不允许p2也说这是我的

因为:

如果两个指针都认为自己拥有释放权:

  • 两边都去delete,Double Free
  • 未定一行为

这也是禁止拷贝,允许移动(转移所有权的本质还是只有一个指针独享资源)的原因

unique_ptr有哪些使用场景?

明确只有一个所有者的资源且需要自动释放

  • 树结构里父节点独占子节点
  • 容器保存的独占对象

手撕一个独占型的智能指针

#include <iostream>
template <typename T>
class UniquePtr {
private:
    T* _ptr;
public:
    // 构造获取资源
    UniquePtr(T* ptr) :_ptr(ptr) { }
    // 析构释放资源
    ~UniquePtr() { if(_ptr) delete _ptr; }

    // 禁止拷贝、赋值重载
    UniquePtr(const UniquePtr& other) = delete;
    UniquePtr& operator=(const UniquePtr& other) = delete;

    // 移动构造,_ptr <- other._ptr
    UniquePtr(UniquePtr&& other) :_ptr(other._ptr) { other._ptr = nullptr; }
    // 移动赋值,我接收别人的所有权,我先把我的资源扔了,再接收被人的资源
    UniquePtr& operator=(UniquePtr&& other) {
        if(this != &other) {
            if(_ptr) delete _ptr;
            _ptr = other._ptr;
            other._ptr = nullptr;
        }
        return *this;
    }
    // 指针行为
    // 解引用,取到具体的值
    T& operator*() const { return *_ptr; }
    // 找地址,取到地址
    T* operator->() const { return _ptr; }
    T* get() const { return _ptr; }
};

shared_ptr 共享型

shared_ptr是一种共享所有权的智能指针,允许有多个shared_ptr共同拥有同一块资源,最后一个拥有者销毁时,释放资源

核心特点

  • 共享所有权:允许有多个shared_ptr共同拥有同一块资源

  • 通过引用计数管理生命周期

    内部维护一个计数器:记录当前有多少个shared_ptr正在拥有这块资源

    • 新拷贝->计数+1
    • 新析构->计数-1
    • 计数减到0,释放资源
  • 开销大于unique_ptr

    需要维护:

    • 引用计数
    • 控制块
    • 多线程环境原子操作

shared_ptr控制块

shared_ptr<int> p1(new int(10));
shared_ptr<int> p2 = p1;
shared_ptr<int> p3 = p2;
  • p1p2p3 各自是不同对象
  • 但它们必须知道自己在管理的是同一块资源
  • 所以它们要共同指向同一个控制块

控制块就是那个“共享管理中心”

控制块核心组件:

  • 强引用计数:拥有资源的指针数量
  • 弱引用计数:观察资源的指针数量

shared_ptr工作流程

  • 创建时
    • 分配资源给指针
    • 创建控制块,强引用计数初始化为1
  • 拷贝时
    • 新指针指向同一块资源和控制块
    • 强引用计数+1
  • 析构时
    • 某个指针离开作用域:
      • 强引用计数-1
      • 如果==0,释放资源,否则不释放
  • 最后一个析构时
    • 强引用计数变为0
    • 删除真正的对象
    • 控制块是否释放,取决于weak_ptr

shared_ptr使用场景

  • 资源需要多方共享
    • 一个对象被多个模块使用
    • 回调要延迟对象存活时间
  • 谁最后结束不确定—>说不清谁负责delete
  • 需要把资源生命周期交给最后一个使用者

shared_ptr线程安全吗?

线程安全部分:不同副本,引用计数++ --线程安全

  • 拷贝
  • 析构
  • 赋值

重点是:shared_ptr被按值捕获了,每个线程操作的是自己的副本,不同时修改同一个shared_ptr变量本体

线程不安全部分:

  • 对象内容并发修改:产生数据竞争
  • 同一个shared_ptr变量并发修改,存在竞争关系

手撕shared_ptr

template <typename T>
class SharedPtr {
private:
    T* _ptr;
    int* _cnt;  // 放到堆上,多个对象共享堆资源

    // 手动实现释放逻辑
    void Release() {
        if(0 == --(*_cnt)) {
            delete _ptr;
            delete _cnt;
        }
    }
public:
    // 构造获取资源,计数++
    SharedPtr(T* ptr = nullptr) : _ptr(ptr), _cnt(new int(1)) { }
    // 析构直接调用Release
    ~SharedPtr() { Release(); }

    // 拷贝构造和赋值
    SharedPtr(const SharedPtr& other) :_ptr(other._ptr), _cnt(other._cnt) {
        ++(*_cnt);
    }
    SharedPtr& operator=(const SharedPtr& other) {
        if(this != &other) {
            // other覆盖_ptr
            Release();
            _ptr = other._ptr;
            _cnt = other._cnt;
            ++(*_cnt);
        }
        return *this;
    }

    // 指针行为
    T& operator*() const { return *_ptr; }
    T* operator->() const { return _ptr; }
    T* get() const { return _ptr; }
    int cnt() const { return *_cnt; }
};

shared_ptr循环引用问题

当多个对象彼此持有shared_ptr,形成强引用环,引用计数永远到不了0,资源无法释放

// 循环引用问题
class B;
class A {
public:
    std::shared_ptr<B> _pb;
};
class B {
public:
    std::shared_ptr<A> _pa;
};
int main() {
    std::shared_ptr<A> a(new A());
    std::shared_ptr<B> b(new B());
    a->_pb = b;
    b->_pa = a;
    return 0;
}

此时:

  • a强持有b
  • b强持有a

成环了,引用计数只能处理树状链式拥有关系,不能处理环状拥有关系

weak_ptr 弱引用型

weak_ptr 是一种弱引用智能指针,它可以观察 shared_ptr 管理的对象,但不拥有对象,不增加强引用计数

它的意义是:我想知道对象还在不在,想访问它,但我不想因为我指着它,它就一直存活

// weak_ptr解决循环引用
class B;
class A {
public:
    std::shared_ptr<B> _pb;
};

class B {
public:
    std::weak_ptr<A> _pa;
};

int main() {
    // A 强计数1 B 强计数1
    std::shared_ptr<A> a(new A());
    std::shared_ptr<B> b(new B());

    // B 强计数2
    a->_pb = b; // b赋给_pb了,_pb是shared,B的强计数+1
    // A 强计数1 弱计数1
    b->_pa = a; // a赋给_pa了,_pa是weak,A的强计数不变
    return 0;
}

环里至少有一条边不能是强引用

总结

  • RAII 是把资源生命周期绑定到对象生命周期上,依赖析构自动释放资源。

  • unique_ptr 表达独占所有权,不能拷贝,只能移动。

  • shared_ptr 表达共享所有权,底层依赖控制块和强引用计数。

  • 循环引用的本质是强引用环导致计数无法归零。

  • weak_ptr 不拥有对象、不增加强计数,通过 lock() 安全访问对象。

Logo

AtomGit 是由开放原子开源基金会联合 CSDN 等生态伙伴共同推出的新一代开源与人工智能协作平台。平台坚持“开放、中立、公益”的理念,把代码托管、模型共享、数据集托管、智能体开发体验和算力服务整合在一起,为开发者提供从开发、训练到部署的一站式体验。

更多推荐