make_shared

  make_shared函数的主要功能是在动态内存中分配一个对象并初始化它,返回指向此对象的shared_ptr;由于是通过shared_ptr管理内存,因此一种安全分配和使用动态内存的方法。

     如下为make_shared的使用:

    //p1指向一个值为"9999999999"的string
    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<string>(10, 9),如果不传递显示 模板实参string类型,make_shared无法从(10, '9')两个模板参数中推断出其创建对象类型。
  3)make_shared在传递参数格式是可变的,参数传递为生成类型的构造函数参数,因此在创建shared_ptr<T>对象的过程中调用了类型T的某一个构造函数。

2.make_shared模板实现
 

make_shared和shared_ptr的区别

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(std::shared_ptr<Lhs> &lhs, std::shared_ptr<Rhs> &rhs){...}

f(std::shared_ptr<Lhs>(new Lhs()),
  std::shared_ptr<Rhs>(new Rhs())
);

因为C++允许参数在计算的时候打乱顺序,因此一个可能的顺序如下:

  1. new Lhs()
  2. new Rhs()
  3. std::shared_ptr
  4. std::shared_ptr

此时假设第2步出现异常,则在第一步申请的内存将没处释放了,上面产生内存泄露的本质是当申请数据指针后,没有马上传给std::shared_ptr,因此一个可能的解决办法是:

auto lhs = std::shared_ptr<Lhs>(new Lhs());
auto rhs = std::shared_ptr<Rhs>(new Rhs());
f(lhs, rhs);

而一个比较好的方法是使用std::make_shared

f(std::make_shared<Lhs>(),
  std::make_shared<Rhs>()
);

make_shared的缺点

因为make_shared只申请一次内存,因此控制块和数据块在一起,只有当控制块中不再使用时,内存才会释放,但是weak_ptr却使得控制块一直在使用。

什么是weak_ptr?

weak_ptr是用来指向shared_ptr,用来判断shared_ptr指向的数据内存是否还存在了(通过方法lock),下面是一段示例代码:

#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主要有两个用途:

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

weak_ptr解决环形依赖

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

include <memory>
include <iostream>

using namespace std;
struct B;
struct A { shared_ptr<B> b;};
struct B { shared_ptr<A> a;};


int main()
{
    shared_ptr<A> x(new A);
    //x->b = new B; // wrong
    //x->b = shared_ptr<B>(new B);
    x->b = make_shared<B>();
    x->b->a = x;
    cout << x.use_count() <<endl;
    cout << x->b.use_count() <<endl;
    // Ref count of 'x' is 2.
    // Ref count of 'x->b' is 1.
    // When 'x' leaves the scope, there will be a memory leak:
    // 2 is decremented to 1, and so both ref counts will be 1.
    // (Memory is deallocated only when ref count drops to 0)
}

下面是解决方案:

shared_ptr<A> x(new A);
//x->b = new B; // wrong
//x->b = shared_ptr<B>(new B);
x->b = make_shared<B>();
x->b->a = x;
cout << x.use_count() <<endl;
cout << x->b.use_count() <<endl;
// Ref count of 'x' is 1.
// Ref count of 'x->b' is 1.
// When 'x' leaves the scope, its ref count will drop to 0.
// While destroying it, ref count of 'x->b' will drop to 0.
// So both A and B will be deallocated.   cout << x->b.use_count() <<endl;

一个自然而然的问题是:weak_ptr是否能够当编程人员不清楚拥有权的情况下解决环形依赖呢?

答案是不能,当对象之间的拥有权不清楚的时候,weak_ptr并不能带来帮助。如果存在环,必须要找出来,然后手动打破。那怎么能够解决环形依赖呢?可以使用有完整垃圾回收机制的语言如Java,Go,Haskell,或者使用有些缺陷的垃圾回收器(C/C++)Boehm GC)

为什么weak_ptr使得控制块一直使用呢?

我们想下,当要使用weak_ptr来获取shared_ptr的时候,需要得到指向数据的shared_ptr数目,而这正是通过user-count来得到的,而这块内存是分配在shared_ptr中的,自然有使用的,那就不会释放了,即使数据引用数为0了,但是由于make_shared()使得数据和控制块一起分配,自然只要有weak_ptr指向了控制块,就不会释放整块内存了。

weak_ptr的使用注意

下面有段代码:

shared_ptr<int> p(new int(5));
weak_ptr<int> q(p);

// some time later

if(int * r = q.get())
{
    // use *r
}

如果在多线程中,在if之后,但是在使用*r之前,另一个线程对p进行了reset,那次后在使用*r则会抛出异常,一个解决方法就是:

shared_ptr<int> p(new int(5));
weak_ptr<int> q(p);

// some time later

if(shared_ptr<int> r = q.lock())
{
    // use *r
}

此时r指向了数据,就不怕被释放了,因此在使用weak_ptr的时候,应使用lock方法转换成shared_ptr后使用。

智能指针使用Tip

2018年04月15日 13:47:17 CPriLuke 阅读数:51 标签: shared_ptr 智能指针 内存管理 更多

个人分类: C/C++

C++11引入shared_ptr,unique_ptr,weak_ptr后,大大简化了c++对动态内存的管理,为了能更好的发挥智能指针的优势,且避免不必要的异常,下面总结了使用智能指针的的一些注意事项:

1.智能指针是行为像指针的类,其本质是一个类,其原理是通过构造/拷贝/赋值/析构操作来维护引用计数,从而达到对资源的管理,且该资源不仅仅限于动态内存;
比如:可以通过智能指针管理tcp的连接与端口;

 
  1. {

  2. int handle = tcp_nenect(ip_addr, port_num);

  3. shared_ptr<int> p((int*)handle, [](int* h) {tcp_disconnect((int)h);}; //

  4.  
  5. //离开作用域时,自动断开连接

  6. ....

  7. }

2.尽量不用get()函数返回智能指针保存的指针,而是直接采用解引用运算符(*/);为了解决这个问题,是需要知道我们在什么情况下使用get(),及其对应的替换方式
情形一:智能指针类型为动态数组,通过get()返回头指针进行指针运算.(智能指针不支持),
此种情形最好将内置数组换成vector或array容器;

 
  1. unique_ptr<int[]> parray(new int[10]);

  2. int *begin = parray.get(); //通过get()返回头指针

  3. int *end = begin + 10;

  4.  
  5. fill(begin, end, 10);

  6. for_each(begin, end, [](const int& p) {

  7. cout<< p << " ";

  8. }); //打印 10 10 10 ...10

  9. cout<<endl;

  10.  
  11. //采用解引用

  12. for(int i = 0;i < 10; i++)

  13. parray[i] = 12;

  14.  
  15. for(int i = 0;i < 10; i++)

  16. cout<< parray[i] << " ";

情形二:通过指针访问指向对象的成员

 
  1. shared_ptr<pair<string, int>> ps = make_shared<pair<string, int>>(string("hello"), 10);

  2. auto* pp = ps.get(); //多此一举,不提倡的用法

  3. cout<< pp->first << " " << pp->second << endl;

  4.  
  5. cout<< ps->first << " " << ps->second << endl; //简单方便

3.当需要管理多个相同元素时,不要用内置数组,建议使用vector或array容器;之所以不使用内置数组,是由于shared_ptr<>不直接支持动态数组管理,且无法高效使用标准库

 
  1. //shared_ptr类型为内置数组必须声明删除器为delete[]

  2. shared_ptr<int> sp(new int[10], [](int*p) { delete[] p; } );

  3.  
  4. //建议方法

  5. shared_ptr<vector<int>> sp = make_shared<vector<int>>(10, 1);

  6. for_each(sp->begin(), sp->end(), [](int &e) {cout<< e << " "; } );

4.尽量不能混用普通指针和智能指针,尤其是以下几点:
  ①不使用相同的内置指针初始化(或reset)多个智能指针,正确方式如下:

 
  1. shared_ptr<int> p(new int(10)); //ok,但提倡使用make_pair

  2. p.reset(new int(42)); //ok

  ②不delete get()返回的指针
  ③不使用get()初始化或reset另一个智能指针
  ④如果使用get()返回的指针,当对应的最后一个智能指针销毁后,此指针就无效了

反正上面四点记也挺麻烦,easy一点,就是别混用两者,甚至只使用智能指针;

5.使用make_pair初始化智能指针更简单,更安全

 
  1. shared_ptr<vector<int>> sp = make_shared<vector<int>>(10, 1); //运用的v(n, )构造

  2.  
  3. shared_ptr<pair<string, int>> ps = make_shared<pair<string, int>>(string("hello"), 10);

  4.  
  5. shared_ptr<vector<int>> sp = make_shared<vector<int>>(vector<int>{1,2,3,4,5}); //不能少vector<int>

6.采用typedef关键字定义智能指针类型避免表达式过长

 
  1. //改写上面的表达式

  2. typedef vector<int> vint ;

  3. typedef shared_ptr<vector<int>> vi_sptr ;

  4.  
  5. vi_sptr sp1 = make_shared<vint>(10, 1);

  6. vi_sptr sp2 = make_share<vint>(vint{1,2,3,4,5});

7.使用weak_ptr前请先lock()

 
  1. //正确使用方式

  2. if(share_ptr<int> np = wp.lock()) //如果np不为空则条件成立

  3. {

  4. //正常使用np进行操作

  5. }

Make_shared

Why Make_shared ?

C++11 中引入了智能指针, 同时还有一个模板函数 std::make_shared 可以返回一个指定类型的 std::shared_ptr, 那与 std::shared_ptr 的构造函数相比它能给我们带来什么好处呢 ?

优点

效率更高

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

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

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

1
2
 
  1. auto p = new widget();

  2. shared_ptr sp1{ p }, sp2{ sp1 };

  3.  

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

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

  2.  

内存分配的动作, 可以一次性完成. 这减少了内存分配的次数, 而内存分配是代价很高的操作.

关于两种方式的性能测试可以看这里 Experimenting with C++ std::make_shared

异常安全

看看下面的代码:

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

  2.  
  3. F(std::shared_ptr<Lhs>(new Lhs("foo")),

  4. std::shared_ptr<Rhs>(new Rhs("bar")));

  5.  

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 没有立即获得裸指针.

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

1
2
3
 
  1. auto lhs = std::shared_ptr<Lhs>(new Lhs("foo"));

  2. auto rhs = std::shared_ptr<Rhs>(new Rhs("bar"));

  3. F(lhs, rhs);

  4.  

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

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

  2.  

缺点

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

make_shared 虽好, 但也存在一些问题, 比如, 当我想要创建的对象没有公有的构造函数时, make_shared 就无法使用了, 当然我们可以使用一些小技巧来解决这个问题, 比如这里 How do I call ::std::make_shared on a class with only protected or private constructors?

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

make_shared 只分配一次内存, 这看起来很好. 减少了内存分配的开销. 问题来了, weak_ptr 会保持控制块(强引用, 以及弱引用的信息)的生命周期, 而因此连带着保持了对象分配的内存, 只有最后一个 weak_ptr 离开作用域时, 内存才会被释放. 原本强引用减为 0 时就可以释放的内存, 现在变为了强引用, 若引用都减为 0 时才能释放, 意外的延迟了内存释放的时间. 这对于内存要求高的场景来说, 是一个需要注意的问题. 关于这个问题可以看这里 make_shared, almost a silver bullet

参考

转载自:http://bitdewy.github.io/blog/2014/01/12/why-make-shared/

猜你喜欢

转载自blog.csdn.net/TuxedoLinux/article/details/85042775