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++允许参数在计算的时候打乱顺序,因此一个可能的顺序如下:
- new Lhs()
- new Rhs()
- std::shared_ptr
- 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主要有两个用途:
- 用来记录对象是否存在了
- 用来解决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的连接与端口;
-
{
-
int handle = tcp_nenect(ip_addr, port_num);
-
shared_ptr<int> p((int*)handle, [](int* h) {tcp_disconnect((int)h);}; //
-
//离开作用域时,自动断开连接
-
....
-
}
2.尽量不用get()函数返回智能指针保存的指针,而是直接采用解引用运算符(*/);为了解决这个问题,是需要知道我们在什么情况下使用get(),及其对应的替换方式
情形一:智能指针类型为动态数组,通过get()返回头指针进行指针运算.(智能指针不支持),
此种情形最好将内置数组换成vector或array容器;
-
unique_ptr<int[]> parray(new int[10]);
-
int *begin = parray.get(); //通过get()返回头指针
-
int *end = begin + 10;
-
fill(begin, end, 10);
-
for_each(begin, end, [](const int& p) {
-
cout<< p << " ";
-
}); //打印 10 10 10 ...10
-
cout<<endl;
-
//采用解引用
-
for(int i = 0;i < 10; i++)
-
parray[i] = 12;
-
for(int i = 0;i < 10; i++)
-
cout<< parray[i] << " ";
情形二:通过指针访问指向对象的成员
-
shared_ptr<pair<string, int>> ps = make_shared<pair<string, int>>(string("hello"), 10);
-
auto* pp = ps.get(); //多此一举,不提倡的用法
-
cout<< pp->first << " " << pp->second << endl;
-
cout<< ps->first << " " << ps->second << endl; //简单方便
3.当需要管理多个相同元素时,不要用内置数组,建议使用vector或array容器;之所以不使用内置数组,是由于shared_ptr<>不直接支持动态数组管理,且无法高效使用标准库
-
//shared_ptr类型为内置数组必须声明删除器为delete[]
-
shared_ptr<int> sp(new int[10], [](int*p) { delete[] p; } );
-
//建议方法
-
shared_ptr<vector<int>> sp = make_shared<vector<int>>(10, 1);
-
for_each(sp->begin(), sp->end(), [](int &e) {cout<< e << " "; } );
4.尽量不能混用普通指针和智能指针,尤其是以下几点:
①不使用相同的内置指针初始化(或reset)多个智能指针,正确方式如下:
-
shared_ptr<int> p(new int(10)); //ok,但提倡使用make_pair
-
p.reset(new int(42)); //ok
②不delete get()返回的指针
③不使用get()初始化或reset另一个智能指针
④如果使用get()返回的指针,当对应的最后一个智能指针销毁后,此指针就无效了
反正上面四点记也挺麻烦,easy一点,就是别混用两者,甚至只使用智能指针;
5.使用make_pair初始化智能指针更简单,更安全
-
shared_ptr<vector<int>> sp = make_shared<vector<int>>(10, 1); //运用的v(n, )构造
-
shared_ptr<pair<string, int>> ps = make_shared<pair<string, int>>(string("hello"), 10);
-
shared_ptr<vector<int>> sp = make_shared<vector<int>>(vector<int>{1,2,3,4,5}); //不能少vector<int>
6.采用typedef关键字定义智能指针类型避免表达式过长
-
//改写上面的表达式
-
typedef vector<int> vint ;
-
typedef shared_ptr<vector<int>> vi_sptr ;
-
vi_sptr sp1 = make_shared<vint>(10, 1);
-
vi_sptr sp2 = make_share<vint>(vint{1,2,3,4,5});
7.使用weak_ptr前请先lock()
-
//正确使用方式
-
if(share_ptr<int> np = wp.lock()) //如果np不为空则条件成立
-
{
-
//正常使用np进行操作
-
}
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 |
|
如果选择使用 make_shared
的话, 情况就会变成下面这样:
1 |
|
内存分配的动作, 可以一次性完成. 这减少了内存分配的次数, 而内存分配是代价很高的操作.
关于两种方式的性能测试可以看这里 Experimenting with C++ std::make_shared
异常安全
看看下面的代码:
1 2 3 4 |
|
C++ 是不保证参数求值顺序, 以及内部表达式的求值顺序的, 所以可能的执行顺序如下:
- new Lhs(“foo”))
- new Rhs(“bar”))
- std::shared_ptr
- std::shared_ptr
好了, 现在我们假设在第 2 步的时候, 抛出了一个异常 (比如 out of memory, 总之, Rhs 的构造函数异常了), 那么第一步申请的 Lhs 对象内存泄露了. 这个问题的核心在于, shared_ptr 没有立即获得裸指针.
我们可以用如下方式来修复这个问题.
1 2 3 |
|
当然, 推荐的做法是使用 std::make_shared
来代替:
1 |
|
缺点
构造函数是保护或私有时,无法使用 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