C++11 智能指针

1. shared_ptr

基于引用计数,


环状引用:

#include <iostream>
#include <memory>
using namespace std;

struct B;

struct A{
    ~A(){
        std::cout << "~A()" << std::endl;
    }

    shared_ptr<B> m_sp;
};

struct B{
    ~B(){
        std::cout << "~B()" << std::endl;
    }

    shared_ptr<A> m_sp;
};

// 注意:这里是动态内存分配,并没有出现"A对象"和"B对象",这里为了叙述方便,借用"对象"的名称;
// 或者说这里所称的"对象"为匿名对象,被指针管理;匿名对象的销亡不受编译器控制,需要手动释放.
void test(){
    shared_ptr<A> spA = make_shared<A>(); // 定义一个A对象,该对象被引用次数为1
    shared_ptr<B> spB = make_shared<B>(); // 定义一个B对象,该对象被引用次数为1

    std::cout << spA.use_count() << std::endl; // 1
    std::cout << spB.use_count() << std::endl; // 1

    spA->m_sp = spB; // spB指向的是B对象,此时A对象的成员m_sp也指向了B对象, 所以此时B对象的被引用次数为2

    std::cout << spA.use_count() << std::endl; // 1
    std::cout << spB.use_count() << std::endl; // 2

    spB->m_sp = spA; // spA指向的是A对象,此时B对象的成员m_sp也指向了A对象, 所以此时A对象的被引用次数为2

    std::cout << spA.use_count() << std::endl; // 2
    std::cout << spB.use_count() << std::endl; // 2
} // (1) spA销亡,A对象的引用次数减1,但是由于B对象还存在,并且B对象的m_sp还指向A对象,
// 所以此时A对象的被引用次数为1,没有减至0,所以A对象的内存没有被释放;
// (2) 接着spB销亡,B对象的引用次数减1,此时B对象的被引用次数由2减至1;
// (3) 注意,此时A对象依然存在,它的成员m_sp指向B对象,B对象的引用次数依然是1,所以B对象依然存在;
// (4) 又因为B对象的存在,B对象的成员m_sp指向A对象,A对象的被引用次数也是1,所以A对象依然存在;
// 所以spA和spB都销亡了,但是A和B对象依然存在并且相互指引.
// 这就是没有A和B对象的引用次数没有减至0的根源,因为A和B对象各自还有持有一个shared_ptr相互指引),造成了内存泄露.


int main() {
    test();

    return 0;
}

2. weak_ptr

主要用来配合shared_ptr使用,能够打破shared_ptr的环状引用问题.它更像一个shared_ptr的助手,而不像一个智能指针,因为它没有指向的特性,没有->和*运算符,也不能直接接受指针,只能通过share_ptr构造或者默认构造/复制构造,当访问其"管理"的内存对象时,也必须临时用lock()成员函数构造一个临时shared_ptr对象.注意:上面第1点提到的环状引用,实际上是存在shared_ptr的,只是无法再访问它们罢了.

#include <iostream>
#include <memory>
using namespace std;

int main(){
    weak_ptr<int> wp1; // 默认构造,没有管理内存

    shared_ptr<int> sp = make_shared<int>(10);
    weak_ptr<int> wp2(sp); // 用shared_ptr进行构造,由weak_ptr管理内存

    weak_ptr<int> wp3(wp2); // 复制构造

    auto sp_temp = wp2.lock(); // lock()临时构造一个shared_ptr对象
    std::cout << *sp_temp << std::endl; // 10

    return 0;
}

weak_ptr并不管理内存,因为它不参与引用计数,也不接受指针,也就无法释放内存;它也没有get()函数可以直接访问内存;

至于它接受shared_ptr参数构造,当shared_ptr的引用计数降为0时,也就把内存释放了,内存是shared_ptr管理的.这就是为什么它没有->和*运算符,访问内存时,必须通过临时构造shared_ptr对象的原因,因为它直接访问内存是不安全的,内存可能已经被释放了.

再看看lock()构造的临时shared_ptr对象时发生的事情:

#include <iostream>
#include <memory>
using namespace std;

int main(){
    shared_ptr<int> sp = make_shared<int>(10);
    weak_ptr<int> wp(sp);
    {
        // 这个判断必不可少,因为weak_ptr.lock()取得的shared_ptr可能是空的(当内存被释放时)
        // 而shared_ptr重载了类型转换运算符operator bool,如果为空, 返回false
        if(shared_ptr<int> p = wp.lock()){ // 临时对象p被构造,int对象的引用计数加1
            std::cout << *p << std::endl;
        }
    } // 离开作用域,临时对象p析构,int对象的引用次数减1,还原到lock调用之前的状态
    
    // (1)可以看到,lock()构造一个shared_ptr临时对象对外界是没有任何影响的
    // (2)其次,既然叫lock(),这个函数是原子性的,是线程安全的
    // (3)如果允许使用get()获得其普通指针,那么无法判断这个内存是否被释放了
    
    return 0;
}

我们来猜测一下weak_ptr可能的实现:

#include <iostream>
using namespace std;

class SharedPtr{
public:
    
private:
    int m_refTimes;
};

class WeakPtr{
public:


private:
    // 注意,在weak_ptr中一定不能有shared_ptr成员,不然就一直有一个shared_ptr指向内存,
    // 这样,只要weak_ptr存在,内存就不能被释放,不符号要求
    int m_refTimes; // 其对应的shared_ptr管理的对象的引用次数
    // 作为配合,shared_ptr也必须要能够管理weak_ptr,所以shared_ptr应该除了对象引用计数外,
    // 还需要记录weak_ptr的引用次数
};

如果仅仅使用weak_ptr是没有意义的:

#include <iostream>
#include <memory>
using namespace std;

struct A{
    ~A(){
        std::cout << "~A()" << std::endl;
    }
};

void fun(){
    weak_ptr<A> wp(make_shared<A>()); // 在"Hello World"打印之前,A对象就被析构了,
// 因为make_shared<A>仅仅构造了一个临时的shared_ptr,
// 本句执行完之后,shared_ptr对象析构,释放其管理的内存
    std::cout << "Hello World" << std::endl; 
}

int main() {
    fun();

    return 0;
}

前面提到:"环状引用的根源在于,两个对象各自持有一个强引用指针来相互指引",

那么,要打破这个环很简单,就是要其中某一个对象不持有强引用指针即可.

这里的"强引用"指的是影响引用计数,换言之,weak_ptr的"弱"引用,最关键在于,它不影响引用计数.

所以,我们可以把上面的代码修改一下,即可消除环状引用:

#include <iostream>
#include <memory>
using namespace std;

struct B;

struct A{
    ~A(){
        std::cout << "~A()" << std::endl;
    }

    shared_ptr<B> m_sp;
};

struct B{
    ~B(){
        std::cout << "~B()" << std::endl;
    }

    weak_ptr<A> m_wp;
};

void test(){
    shared_ptr<A> spA = make_shared<A>();
    shared_ptr<B> spB = make_shared<B>();

    std::cout << spA.use_count() << std::endl;
    std::cout << spB.use_count() << std::endl;

    spA->m_sp = spB;
    spB->m_wp = spA; // (spB->m_wp)(spA);

    std::cout << spA.use_count() << std::endl;
    std::cout << spB.use_count() << std::endl;
}

int main() {
    test();

    shared_ptr<A> spA = make_shared<A>();
    weak_ptr<A> wpA(spA);

    return 0;
}

参考资料:

用weak_ptr解决shared_ptr的环形引用问题_唐唐唐唐人的博客-CSDN博客_weak_ptr如何解决环形引用

C++ 三种智能指针的使用场景(unique_ptr vs shared_ptr vs weak_ptr) - 零壹生万物

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值