C/C++编程:理解make_shared

1059 篇文章 290 订阅
本文介绍了make_shared的作用,如何在C++中高效地动态创建并初始化对象,以及它与std::shared_ptr构造函数的区别。重点讨论了make_shared在内存分配、效率、异常安全和环形引用解决方案上的优点和局限性。
摘要由CSDN通过智能技术生成

为什么要使用make_shared

  • shared_ptr:可以指向特定类型的对象,用于自动释放所指的对象
  • make_shared:功能是在动态内存中分配一个对象并初始化它,返回指向此对象的shared_ptr

shared_ptr<string> p1 = make_shared<string>(10, '9');  
 
shared_ptr<string> p2 = make_shared<string>("hello");  
 
shared_ptr<string> p3 = make_shared<string>(); 

从上面可以看出
1)make_shared是一个模板函数;
2)make_shared必须显式指定想要创建的对象类型,如上题所示make_shared(10, 9),如果不传递显式模板实参string类型,make_shared无法从(10, ‘9’)两个模板参数中推断出其创建对象类型。
3)make_shared在传递参数格式是可变的,参数传递为生成类型的构造函数参数,因此在创建shared_ptr对象的过程中调用了类型T的某一个构造函数。

make_shared模板实现

template<typename _Tp, typename... _Args>
inline shared_ptr<_Tp>
make_shared(_Args&&... __args)
{
  typedef typename std::remove_const<_Tp>::type _Tp_nc;
  return std::allocate_shared<_Tp>(std::allocator<_Tp_nc>(),
				   std::forward<_Args>(__args)...);
}
 
template<typename _Tp, typename _Alloc, typename... _Args>
inline shared_ptr<_Tp>
allocate_shared(const _Alloc& __a, _Args&&... __args)
{
  return shared_ptr<_Tp>(_Sp_make_shared_tag(), __a,
			 std::forward<_Args>(__args)...);
}

我们依次分析上述的关键代码


//关键行1
template<typename _Tp, typename... _Args>
inline shared_ptr<_Tp> make_shared(_Args&&... __args)  
  
//关键行2
std::forward<_Args>(__args)...
 
//关键行3
return shared_ptr<_Tp>(_Sp_make_shared_tag(), __a,
			 std::forward<_Args>(__args)...);

从上面关键代码可以看出:make_shared是组合可变参数模板与forward(转发)机制实现将实参保持不变的传递给其他函数

  • 使用可变参数:是因为string有多个构造函数,而且参数各不相同
  • _Args参数为右值引用(Args&&)和std::forward:是为了保持实参中类型信息的传递。这样当传递一个右值string&&对象给make_shared时,就可以使用string的移动构造函数进行初始化。

此外std::forward<_Args>(__args)...是采用 包扩展形式调用的,原理如下:

shared_ptr<string> p1 = make_shared<string>(10, '9'); 
 
//扩展如下,对两个参数分别调用std::forward
return shared_ptr<string>(_Sp_make_shared_tag(), _a ,
			  std::forward<int>(10),  
			  std::forward<char>(c));

make_shared VS std::shared_ptr区别

为什么不适用std::shared_ptr 的构造函数的构造函数,而是要用std::make_shared呢?

优点

效率更高

shared_ptr需要维护引用计数的信息。

  • 强引用,用来记录当前有多少个存活的shared_ptr正在持有该对象,共享的对象会在最后一个强引用离开的时候销毁(也可能释放)
  • 弱引用,用来记录当前有多少个正在观察该对象的weak_ptr,当最后一个弱引用离开的时候,共享的内部信息控制块会被销毁和释放(共享的对象也会被释放,如果还没有释放的话)

如果你通过使用原始的new表示分配对象,然后传递给shared_ptr(也就是shared_ptr的构造函数)的话,shared_ptr的实现没有办法选择, 而只能单独的分配控制块:

auto p = new widget();
shared_ptr sp1{ p }, sp2{ sp1 };

在这里插入图片描述

如果选择使用 make_shared 的话, 情况就会变成下面这样:

auto sp1 = make_shared(), sp2{ sp1 };

在这里插入图片描述
从上面可以看出,内存分配的动作,可以一次性完成,这减少了内存分配的次数,而内存分配是代价很高的操作。

总结:

struct A;
std::shared_ptr<A> p1 = std::make_shared<A>();
std::shared_ptr<A> p2(new A);

上面两者的区别:

  • std::shared_ptr构造函数会执行两次内存申请,而std::make_shared则执行一次
  • std::shared_ptr在实现的时候使用refcount技术,因此内部会有一个计数器(控制块)用来管理数据和一个指针。因此在std::shared_ptr<A> p2(new A)的时候,首先会申请数据的内存,然后申请内控制块,因此是两次内存申请,而std::make_shared<A>()则是只执行一次内存申请,将数据和控制块的申请放到一起。那这一次和两次的区别会带来什么不同的效果呢?

看下面的代码

void F(const std::shared_ptr<Lhs>& lhs, const std::shared_ptr<Rhs>& rhs) { /* ... */ }

F(std::shared_ptr<Lhs>(new Lhs("foo")),
  std::shared_ptr<Rhs>(new Rhs("bar")));

C++是不保证参数求值顺序,以及内部表示的求值顺序的,所以可能的执行顺序如下:

  1. new Lhs(“foo”))
  2. new Rhs(“bar”))
  3. std::shared_ptr
  4. std::shared_ptr

好了, 现在我们假设在第 2 步的时候, 抛出了一个异常 (比如 out of memory, 总之, Rhs 的构造函数异常了), 那么第一步申请的 Lhs 对象内存泄露了. 这个问题的核心在于, shared_ptr 没有立即获得裸指针.

我们可以用如下方式来修复这个问题.

auto lhs = std::shared_ptr<Lhs>(new Lhs("foo"));
auto rhs = std::shared_ptr<Rhs>(new Rhs("bar"));
F(lhs, rhs);

当然, 推荐的做法是使用 std::make_shared 来代替:

F(std::make_shared<Lhs>("foo"), std::make_shared<Rhs>("bar"));

总结: make_ptr的优点是:shared_ptr的构造函数会申请两次内存,而make_ptr会申请一次内存。一方面效率提交了,一方面保证了异常安全

缺点

构造函数是保护或者私有时,无法使用make_shared

当我想要创建的对象没有共有的构造函数时,make_shared就无法使用了。当然,我们可以使用一些小技巧来解决这个问题

#include <memory>

class A
{
public:
    static std::shared_ptr<A> create()
    {
        struct make_shared_enabler : public A {};

        return std::make_shared<make_shared_enabler>();
    }

private:
    A() {}  
};

对象的内存可能无法及时回收

因为make_shared只申请一次内存,因此控制块和数据块在一起,只有但控制块中不再使用时,内存才会释放。但是如果还有weak_ptr指向该块对象所在的内存,存放管理对象的部分内存仍然不会被释放,因而导致在所有其他weak_ptr销毁前整块内存(尽管被管理对象已经析构了)将不会进入系统的内存池循环使用

什么是weak_ptr

weak_ptr是用来指向shared_ptr的,用来判断shared_ptr指向的数据是否还存在。看个示例代码:

#include <memory>
#include <iostream>
using namespace std;
struct A{
    int _i;
    A(): _i(int()){}
    A(int i): _i(i){}
};
 
int main()
{
    shared_ptr<A> sharedPtr(new A(2));
    weak_ptr<A> weakPtr = sharedPtr;
    sharedPtr.reset(new A(3)); // reset,weakPtr指向的失效了。
    cout << weakPtr.use_count() <<endl;
}

通过lock()来判断是否存在了,lock()相当于

expired()?shared_ptr<element_type>()shared_ptr<element_type>(*this)

当不存在的时候,会返回一个空的shared_ptr,weak_ptr在指向shared_ptr的时候,并不会增加ref count,因此weak_ptr主要有两个用途:

  • 用来记录对象是否存在了
  • 用来解决shared_ptr环形依赖的问题

下面是存在环形依赖的代码:

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

class father;
class son;



class father
{
public:
    father() {
        cout << "father !" << endl;
    }
    ~father() {
        cout << "~~~~~father !" << endl;
    }
    void setSon(shared_ptr<son> s) {
        m_son = std::move(s);
    }
    shared_ptr<son> m_son;
};
class son
{
public:
    son() {
        cout << "son !" << endl;
    }
    ~son() {
        cout << "~~~~~~son !" << endl;
    }
    void setFather( shared_ptr<father> f) {
        m_father = std::move(f);
    }
    shared_ptr<father> m_father;
};



void test() {
    shared_ptr<father>  f(new father());
    shared_ptr<son> s(new son);
    f->m_son= s;
    s->m_father= f;
}

int main()
{
    test();
    return 0;
}

在这里插入图片描述
可以看到,当test函数执行结束的时候,并没有自动的把f和s析构掉,这是因为f和s内部的智能指针互相指向了对方,导致自己的引用计数一直为1,所以没有进行析构,这就造成了内存泄漏。

weak_ptr

为了避免shared_ptr环形引用的问题,需要引入一个弱引用weak_ptr,weak_ptr的为了配合shared_ptr而引入的一种智能指针,它更像是shared_ptr的一个助手而不是智能指针,因为它不具有普通指针的行为,没有重载operator*和->,它的最大作用是协助shared_ptr工作,像旁观者那样观测资源的使用情况.

为了解决上面例子中的环形引用问题,可以把当中的一个shared_ptr换成weak_ptr:

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

class father;
class son;



class father
{
public:
    father() {
        cout << "father !" << endl;
    }
    ~father() {
        cout << "~~~~~father !" << endl;
    }
    void setSon(shared_ptr<son> s) {
        m_son = s;
    }
    //shared_ptr<son> m_son;
    weak_ptr<son> m_son; // 用weak_ptr来替换
};
class son
{
public:
    son() {
        cout << "son !" << endl;
    }
    ~son() {
        cout << "~~~~~~son !" << endl;
    }
    void setFather( shared_ptr<father> f) {
        m_father = std::move(f);
    }
    shared_ptr<father> m_father;
};



void test() {
    shared_ptr<father>  f(new father());
    shared_ptr<son> s(new son);
    f->m_son= s;
    s->m_father= f;
}

int main()
{
    test();
    return 0;
}

在这里插入图片描述

注意:

  • weak_ptr虽然是一个模板类,但是不能用来直接定义指向原始指针的对象
  • weak_ptr接受shared_ptr类型的变量赋值,但是反过来是不行的,需要使用lock函数
  • weak_ptr设计之初就是为了服务与shared_ptr的,所以不增加引用计数就是它的核心功能
  • 由于不知道什么时候weak_ptr所指向的对象就会被析构调,所以使用之前先eapired函数检测一下

面试题:make_shared有用过吗?为什么要用,有什么优缺点

优点是:

  • 它分配的时候,只分配一次,而shared_ptr的构造函数需要分配两次。效率更高更安全

缺点是:

  • 如果有weak_ptr不推荐使用,否则会出现知道最后一个weak_ptr被释放了才真正去释放管理的对象
  • 23
    点赞
  • 108
    收藏
    觉得还不错? 一键收藏
  • 1
    评论
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值