Skip to content

智能指针

约 1966 字大约 7 分钟

C++

2024-11-01

RAII思想

RAII(Resource Acquisition Is Initialization)是一种使用对象生存周期管理资源的技术。

说来简单,就是在对象构造时获取资源,在析构时释放资源的策略。在C++标准库中非常多场景下都有使用,

例如:容器在初始化时申请堆空间,并在生命周期结束时在析构函数中释放堆空间;互斥锁在构造时加锁,在析构时解锁等等。

class A {
    public:
    A(int a) {
        _p = new int(a); // 构造时分配资源
    }
    A(int* b = nullptr) {
        _p = b; //构造时接管外部资源
    }
    /* ... */
    ~A() {
        if(_p) delete _p;
    }
    private:
    int * _p;
};

int main() {
    A a(10);
    A b = new int(20); // 不需要手动 delete
}

智能指针

C++11 引入了unique_ptrshared_ptrweak_ptr智能指针,用以方便管理资源,可以当做指针来使用。unique_ptrshared_ptr重载了*->运算符以及operator bool()运算符。weak_ptr则是为了解决shared_ptr的循环引用问题。

unique_ptr

  • 删除拷贝构造函数和拷贝赋值函数,保证拥有资源的唯一所有权。
  • 支持移动语义,即可以转移资源所有权。
构造
int main() {
    // 报错,因为这里发生了隐式转换,int * --> unique_ptr<int>
    // 然后通过拷贝构造,但是拷贝构造函数被删除了
    std::unique_ptr<int> p0 = new int(10);

    // 正确,直接调用的移动构造函数
    std::unique_ptr<int> p1(new int(10));

    // 推荐,make_unique是C++14引入的,可以避免隐式转换的问题,并且可以保证异常安全
    // 内部使用了new表达式
    std::unique_ptr<int> p2 = std::make_unique<int>(10);

    return 0;
}

shared_ptr

  • 允许多个shared_ptr共享资源,使用引用计数来管理资源的生命周期。
  • 只有拷贝构造和赋值构造函数以及release()成员函数会改变引用计数。
  • 实现了拷贝构造函数、重载赋值运算符。
  • use_count()成员函数可以获取引用计数。
int main() {
    std::shared_ptr<int[]> p0(new int[3]{1, 2, 3});
    std::shared_ptr<int[]> p1 = p0; // 正确
    std::shared_ptr<int[]> p2(p0); // 正确

    p2.use_count(); // 返回引用计数
    p2.release(); // 返回裸指针,引用计数减一
    p2.reset(); // 释放资源,引用计数减一
    return 0;
}

std::make_shared

当使用形如std::shared_ptr<T> p(new T());的方式构造shared_ptr时,控制块和资源是分开存储的。

而使用std::make_shared<T>()则可以保证控制块和资源在堆空间中是连续分布的,减少了内存碎片。

shared_ptr可能引发的问题

重复释放1
int main() {
    std::shared_ptr<int> p1 = std::make_shared<int>(10);
    std::shared_ptr<int> p2(p1.get());
}

// 只有拷贝构造才会增加引用计数,这里会创建两个控制块指向同一个资源,且引用计数均为 1。
// 当p1和p2析构时,都会尝试释放资源,导致重复释放。

weak_ptr

实际上shared_ptr最大的问题就是引用计数无法降为0,导致内存泄露。

  • weak_ptr则是shared_ptr的弱引用辅助工具,weak_ptr访问shared_ptr管理的资源,但是不会增加引用计数。所以很多时候weak_ptr充当shared_ptr的观察者。

  • weak_ptr私有化了*->运算符,需要通过lock()成员函数转换为shared_ptr才能访问资源。

int main() {
    std::shared_ptr<int> sp = std::make_shared<int>(10);
    // shared_ptr可以隐式转换成weak_ptr
    std::weak_ptr<int> wp = sp; 
    // 引用计数为 1。
    cout << wp.use_count() << endl; 

    // shared_ptr不能直接接收weak_ptr,
    // 需要通过weak_ptr的lock()成员函数转换为shared_ptr。
    std::shared_ptr<int> sp2 = wp.lock();

    return 0;
}

可以通过weak_ptr解决循环引用问题。

循环引用1
using std::shared_ptr;
using std::weak_ptr;
struct B;
struct A {
    A(int t):a(t), p(nullptr) {
        cout << "A构造" << endl;
    }
    ~A() {
        cout << "A析构" << endl;
    }
    void set_ptr(shared_ptr<B> b) {
        p = b;
    }

    int a;
    weak_ptr<B> p; 
};

struct B {
    B(int t):b(t), p(nullptr) {
        cout << "B构造" << endl;
    }
    ~B() {
        cout << "B析构" << endl;
    }
    void set_ptr(shared_ptr<A> a) {
        p = a;
    }

    int b;
    weak_ptr<A> p;
};

int main()
{
    shared_ptr<A> a = make_shared<A>(10);
    shared_ptr<B> b = make_shared<B>(12);
    a->set_ptr(b);
    b->set_ptr(a);
    return 0;
}

enable_shared_from_this模板、shared_from_this()成员函数

C++11标准提供了一个类模板,其中提供了一个shared_from_this()公有成员函数,专门给类用来获取指向自身的shared_ptr

  • shared_from_this()只有在类实例已经被shared_ptr管理时才能调用,否则会抛出异常。
struct C: public std::enable_shared_from_this<C> {
    C(int t):c(t) {}
    shared_ptr<C> ptr() {
        return shared_from_this();
    }
    int c;
}

int main() {
    C c(10);
    c.ptr(); //报错,c 未被shared_ptr接管

    shared_ptr<C> c = make_shared<C>(10);
    c->ptr(); //正确,c被shared_ptr接管
    return 0;
}