为什么要使用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++是不保证参数求值顺序,以及内部表示的求值顺序的,所以可能的执行顺序如下:
- new Lhs(“foo”))
- new Rhs(“bar”))
- std::shared_ptr
- 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被释放了才真正去释放管理的对象