C++:共享指针(shared_ptr)详解

shared_ptr是C++11提供的另外一种常见的智能指针,与unique_ptr独占对象所有权不同,shared_ptr允许多个指针指向同一个对象。

每个shared_ptr对象都有一个关联的计数器,被称为引用计数,用来记录有多少个shared_ptr指向所管理的内存对象。这个计数器是线程安全的。每当多一个智能指针一个对象时,指向该对象的所有智能指针内部的引用计数加1,每当减少一个智能指针指向该对象时,引用计数会减1,当计数为0的时候会自动的释放动态分配的资源。

创建shared_ptr对象

std::shared_ptr<int> p1(new int(1));
std::shared_ptr<int> p2=std::make_shared<int>(2);

默认初始化的智能指针中保存着一个空指针

共享对象

我们可以通过拷贝和赋值操作实现多个shared_ptr共享一个资源对象

std::shared_ptr<int> p2(new int(2));
std::shared_ptr<int> p3=p2;

当拷贝一个shared_ptr时,对于被拷贝的shared_ptr所指向的对象来说,其引用计数会增加,通常来说有3种常见的情况:

  • 使用一个shared_ptr去初始化另一个shared_ptr,会拷贝参数的shared_ptr对象。
  • 将它作为函数参数,传递给一个函数时。
  • 将它作为函数返回值,也会发生拷贝。

计数器较少的情况:

  • shared_ptr销毁时,比如离开其作用域,会触发其析构函数,这时所管理对象的引用计数会减一。
  • 当给shared_ptr赋予一个新值时,其原来所指向的对象的引用计数会减一

指定删除器

使用shared_ptr管理非new对象或者是没有析构函数的类时,应该为其传递合适的删除器

#include <iostream>
 #include <memory>
 using namespace std;
 
 void DeleteIntPtr(int *p) {
     cout << "call DeleteIntPtr" << endl;
     delete p;
 }
 
 int main()
 {
     std::shared_ptr<int> p(new int(1), DeleteIntPtr);

     std::shared_ptr<int> p2(new int(1), [](int *p) {
         cout << "call lambda1 delete p" << endl;
         delete p;});

    //p3没有显式指定类型为数组类型int[],shared_ptr默认调用delete
    //而非delete[]来删除他管理的对象,为了正确删除,需要自定义删除器
     std::shared_ptr<int> p3(new int[10], [](int *p) {
         cout << "call lambda2 delete p" << endl;
         delete [] p; // 数组删除
     });

     return 0;
 }

智能指针什么时候需要指定删除器:

在需要 delete 以外的析构行为的时候用. 因为 shared_ptr 在引用计数为 0 后默认调用 delete ptr; 如果不满足需求就要提供定制的删除器.

一些场景:

  • 资源不是 new 出来的(一般也意味着不能 delete), 比如可能是 malloc 出来的
  • 资源是被第三方库管理的 (第三方提供 资源获取 和 资源释放 接口, 那么要么写一个 wrapper 类要么就提供定制的 deleter)
  • 资源不是 RAII 的, 意味着析构函数不会把资源完全释放掉...也就是单纯 delete 还不够, 还得做额外的操作比如你的 end_connection 的例子. 虽然我觉得这个是 bad practice

循环引用

//定义A,拥有B类型指针
class A {
public:
	std::shared_ptr<B> pb;
	~A() {
		std::cout << "~A" << std::endl;
	}
};

//定义B,拥有A类型的指针
class B {
public:
	std::shared_ptr<A> pa;
	~B() {
		std::cout << "~B" << std::endl;
	}
};
void Test(){
    std::shared_ptr<A> pA= std::make_shared<A>();
	std::shared_ptr<B> pB= std::make_shared<B>();
    //pA内部指向pB
	pA->pb = pB;
    //pb内部执行pa
	pB->pa = pA;
}

int main(){
    //会导致循环引用,2个堆内存对象无法被释放
	Test();
	return 0;
}

Test函数结束时,局部变量的销毁是按照其创建顺序的相反顺序来进行销毁的。pB先于pA销毁。当pB销毁时,会先调用pB的析构函数,它会检测到它所指向的对象有2个引用者,即pB和pA的成员pb,引用计数为2,离开作用域后,pB的引用计数-1,并不是0,跟据shared_ptr的规则,pB所指向的内存不会被释放。pA同理。pA和pB所指向的内存都没有得到释放,会发生内存泄漏。

解决方法是将A或B的成员设定为weak_ptr

weak_ptr

weak_ptr也是一种智能指针,通常配合shared_ptr一起使用。

weak_ptr是一种不控制所指向对象生存期的智能指针,它指向由一个shared_ptr管理的对象。所以需要使用一个shared_ptr来初始化一个weak_ptr,并且将一个weak_ptr绑定到一个shared_ptr不会改变shared_ptr的引用计数

它的最大特点是:一旦最后一个指向对象的shared_ptr被销毁,该对象就会被销毁,即使还有weak_ptr指向该对象,所有weak_ptr都会变成nullptr

所以,有时会出现weak_ptr还指向着对象,但是该对象已经被销毁了的情况。不能直接通过weak_ptr访问其所指向的对象。我们以利用expired()方法来判断这个weak_ptr是否已经失效。

我们可以通过weak_ptrlock()方法来获得一个指向共享对象的shared_ptr。如果weak_ptr已经失效,lock()方法将返回一个空的shared_ptr

std::shared_ptr<int> p1= std::make_shared<int>(2);

std::weak_ptr<int> wp(p1);

// 通过lock创建一个对应的shared_ptr
if (auto p = wp.lock()) {
    std::cout << "shared_ptr value: " << *p << std::endl;
    std::cout << "shared_ptr use_count: " << p.use_count() << std::endl;
} else {
    std::cout << "wp is expired" << std::endl;
}

// 释放shared_ptr指向的资源,此时weak_ptr失效
p1.reset();
std::cout << "wp is expired: " <<  wp.expired() << std::endl;

注意事项

不要混用普通指针和智能指针

void TestShared(std::shared_ptr<int> p) {
    ...

}//离开作用域时,p会被销毁

int main()
{
	int* p1 = new int(2);
	TestShared(std::shared_ptr<int> (p1));

	//指向的对象已经被delete,p1是一个空悬指针
	std::cout << *p1<< std::endl;
	return 0;
}

对象的引用计数是其shared_ptr的个数,当一个共享对象的shared_ptr为0时,即使有普通指针还在指向它,也会被释放

不要使用使用get初始化另一个智能指针或为智能指针赋值。

std::shared_ptr<int> p1(new int(1));
std::shared_ptr<int> p2(p1.get());
std::cout << p1.use_count() << " " << p2.use_count() <<std:: endl;

打印会发现它们的引用计数为1,因为引用计数是分开计数的,当其中一类的shared_ptr的引用计数为0时,就会释放对象内存,这时其他shared_ptr就是空悬指针了,此时会出现double free问题。

性能

  1. 内存占用高
    shared_ptr 的内存占用是裸指针的两倍。因为除了要管理一个裸指针外,还要维护一个引用计数。
    因此相比于 unique_ptr, shared_ptr 的内存占用更高

  2. 原子操作性能低
    考虑到线程安全问题,引用计数的增减必须是原子操作。而原子操作一般情况下都比非原子操作慢。

  3. 使用移动优化性能
    shared_ptr 在性能上固然是低于 unique_ptr。而通常情况,我们也可以尽量避免 shared_ptr 复制。
    如果,一个 shared_ptr 需要将所有权共享给另外一个新的 shared_ptr,而我们确定在之后的代码中都不再使用这个 shared_ptr,那么这是一个非常鲜明的移动语义。
    对于此种场景,我们尽量使用 std::move,将 shared_ptr 转移给新的对象。因为移动不用增加引用计数,性能比复制更好。

template<class T>
class SharedPtr{
private:
    T* m_p;
    int* m_count;
        
    void clear() {
        if(m_count&& --(*m_count)==0){
            delete m_p;
            m_p=nullptr;
            delete m_count;
            m_count=nullptr;
        }
    }

public:
    SharedPtr(T* ptr=nullptr):m_p(p),m_count(new int(1)){
    }
    
    ~SharedPtr()
    {
        clear();
    }
    
    //拷贝构造
    SharedPtr(const SharedPtr& that):m_p(that.m_p),m_count(that.m_count){
        ++(*m_count);
    }

    //拷贝赋值
    SharedPtr& operator=(const SharedPtr& that){
        if(m_p!=that.m_p){
            clear();
            m_p=that.m_p;
            m_count=that.m_count;
            ++(*m_count);  
        }
        return *this;
    }
    
    //移动构造
    SharedPtr(SharedPtr&& that):m_p(that.m_p),m_count(that.m_count) {
        that.m_p=nullptr;
        that.m_count=nullptr;
    }

    //移动赋值
    SharedPtr& operator=(SharedPtr&& that){
        clear();
        m_p=that.m_p;
        m_count=that.m_count;
        that.m_p=nullptr;
        that.m_count=nullptr;
        return *this;
    }

    T& operator*(){
        return *m_p;
    }

    T* operator->(){
        return m_p;
    }

    int get_count() const {
        return *m_count;
    }
     
    
};

  • 20
    点赞
  • 6
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值