指针是C/C++区别于其他语言的最强大的语法特性,借助指针,C/C++可以直接操纵内存内容。但是,指针的引入也带来了一些使用上的困难,这要求程序员自己必须手动地对分配申请的内存区进行管理。
本文实例源码github地址:https://github.com/yngzMiao/yngzmiao-blogs/tree/master/2020Q2/20200427。
智能指针的行为类似于常规指针,重要的区别是它负责自动释放所指向的对象。新标准提供的两种重要的智能指针的区别在于管理底层指针的方式:shared_ptr允许多个指针指向同一个对象;unique_ptr则独占所指向的对象。
shared_ptr
使用引用计数,每一个shared_ptr的拷贝都指向相同的内存。每使用它一次,内部的引用计数加1,每析构一次,内部的引用计数减1,减为0时,释放所指向的堆内存。shared_ptr内部的引用计数是安全的,但是对象的读取需要加锁。
shared_ptr有如下几种初始化方式:
make_shared
构造,在C++11版本中就已经支持了。例如:
#include
#include
class Frame {};
int main()
{
std::shared_ptr<Frame> f(new Frame()); // 裸指针直接初始化
std::shared_ptr<Frame> f1 = new Frame(); // Error,explicit禁止隐式初始化
std::shared_ptr<Frame> f2(f); // 拷贝构造函数
std::shared_ptr<Frame> f3 = f; // 拷贝构造函数
f2 = f; // copy赋值运算符重载
std::cout << f3.use_count() << " " << f3.unique() << std::endl;
std::shared_ptr<Frame> f4(std::move(new Frame())); // 移动构造函数
std::shared_ptr<Frame> f5 = std::move(new Frame()); // Error,explicit禁止隐式初始化
std::shared_ptr<Frame> f6(std::move(f4)); // 移动构造函数
std::shared_ptr<Frame> f7 = std::move(f6); // 移动构造函数
std::cout << f7.use_count() << " " << f7.unique() << std::endl;
std::shared_ptr<Frame[]> f8(new Frame[10]()); // Error,管理动态数组时,需要指定删除器
std::shared_ptr<Frame> f9(new Frame[10](), std::default_delete<Frame[]>());
auto f10 = std::make_shared<Frame>(); // std::make_shared来创建
return 0;
}
可以看出,shared_ptr和unique_ptr在初始化的方式上就有一些差别:尽管两者都不支持隐式初始化,但是unique_ptr不支持拷贝构造和拷贝赋值,而shared_ptr都支持。除此之外,由于删除器不是shared_ptr类型的组成部分,在管理动态数组的时候,shared_ptr需要指定删除器。
删除器可以是普通函数、函数对象和lambda表达式等。默认的删除器为std::default_delete
,其内部是通过delete
来实现功能的。
与unique_ptr不同,删除器不是shared_ptr类型的组成部分。也就是说,两个shared_ptr,就算sp1和sp2有着不同的删除器,但只要两者的类型是一致的,都可以被放入vector
#include
#include
#include
class Frame {};
int main()
{
auto del1 = [](Frame* f){
std::cout << "delete1" << std::endl;
delete f;
};
auto del2 = [](Frame* f){
std::cout << "delete2" << std::endl;
delete f;
};
std::shared_ptr<Frame> f1(new Frame(), del1);
std::shared_ptr<Frame> f2(new Frame(), del2);
std::unique_ptr<Frame, decltype(del)> f3(new Frame(), del);
std::vector<std::shared_ptr<Frame> > v;
v.push_back(f1);
v.push_back(f2);
return 0;
}
可以很明显地看出,unique_ptr需要指定原指针的指向类型,还需要指定删除器类型;但shared_ptr只需要指定原指针的指向类型即可。
与std::unique_ptr不同,自定义删除器不会改变std::shared_ptr的大小。其始终是祼指针大小的两倍。
shared_ptr的内存模型如下图:
由图可以看出,shared_ptr包含了一个指向对象的指针和一个指向控制块的指针。每一个由shared_ptr管理的对象都有一个控制块,它除了包含强引用计数、弱引用计数之外,还包含了自定义删除器的副本和分配器的副本以及其他附加数据。
控制块的创建规则:
因此,尽可能避免将裸指针传递给一个std::shared_ptr的构造函数,常用的替代手法是使用std::make_shared。如果必须将一个裸指针传递给shared_ptr的构造函数,就直接传递new运算符的结果,而非传递一个裸指针变量。并且,不要将this指针返回给shared_ptr。当希望将this指针托管给shared_ptr时,类需要继承自std::enable_shared_from_this,并且从shared_from_this()中获得shared_ptr指针。
也就是说:
#include
#include
class Frame {};
int main()
{
Frame* f1 = new Frame();
std::shared_ptr<Frame> f2(f1);
std::shared_ptr<Frame> f3(f1); // Error
return 0;
}
尽量不要使用相同的原始指针来创建多个shared_ptr对象,因为在这种情况下,不同的shared_ptr对象不会知道它们与其他shared_ptr对象共享指针。通俗一点解释,就是,f2和f3两个shared_ptr拥有两个控制块,且这两个控制块同时指向f1指针指向的内存区。这是很危险的,极可能出现重复释放空间的情况。
而make_shared没有临时的裸指针,就不会在写法上出现这种情况。
GCC编译器中,make_shared内部是通过调用std::allocate_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)...);
}
与new相比,make系列函数的优势:
make_shared与new方式内存分布对比图:
make系列函数的局限:
shared_ptr中的引用计数直接关系到何时是否进行对象的析构,因此它的变动尤其重要。
sp1=sp2
时,将修改sp1使得其指向sp2所指的对象。而最初sp1所指向的对象的引用计数递减,同时sp2所指向的对象引用计数递增);需要注意的是:shared_ptr的引用计数本身是安全且无锁的,但对象的读写则不是,因为shared_ptr有两个数据成员,读写操作不能原子化。
上文提到:不要将this指针返回给shared_ptr。当希望将this指针托管给shared_ptr时,类需要继承自std::enable_shared_from_this
,并且从shared_from_this()
中获得shared_ptr指针。
这里对此进行详细讲解:
#include
#include
class Frame {
public:
std::shared_ptr<Frame> GetThis() {
return std::shared_ptr<Frame>(this);
}
};
int main()
{
std::shared_ptr<Frame> f1(new Frame());
std::shared_ptr<Frame> f2 = f1->GetThis();
std::cout << f1.use_count() << " " << f2.use_count() << std::endl;
std::shared_ptr<Frame> f3(new Frame());
std::shared_ptr<Frame> f4 = f3;
std::cout << f3.use_count() << " " << f4.use_count() << std::endl;
return 0;
}
对于这段代码,打印值是什么呢?编译并运行的结果为:
yngzmiao@yngzmiao-virtual-machine:~/test/$ ./main
1 1
2 2
你可能觉得奇怪,GetThis返回的不就是f1本身么,为什么f2和f4的结果就有这样的区别呢?
其实,直接从this指针创建,会为this对象创建新的控制块!也就相当于从裸指针重新创建一个新的控制块出来。
为了解决这个问题,标准库提供了一个方法:让类派生自一个模板类:enable_shared_from_this
。然后调用shared_from_this()
函数即可。
通过调用shared_from_this()成员函数获得一个和this指针指向相同对象的shared_ptr。
class Frame : public std::enable_shared_from_this<Frame> {
public:
std::shared_ptr<Frame> GetThis() {
return shared_from_this();
}
};
这样做的原理是什么呢?
template<typename _Tp>
class enable_shared_from_this
{
protected:
constexpr enable_shared_from_this() noexcept { }
enable_shared_from_this(const enable_shared_from_this&) noexcept { }
enable_shared_from_this& operator=(const enable_shared_from_this&) noexcept { return *this; }
~enable_shared_from_this() { }
public:
shared_ptr<_Tp> shared_from_this()
{ return shared_ptr<_Tp>(this->_M_weak_this); }
shared_ptr<const _Tp> shared_from_this() const
{ return shared_ptr<const _Tp>(this->_M_weak_this); }
private:
template<typename _Tp1>
void _M_weak_assign(_Tp1* __p, const __shared_count<>& __n) const noexcept
{ _M_weak_this._M_assign(__p, __n); }
template<typename _Tp1, typename _Tp2>
friend void __enable_shared_from_this_helper(const __shared_count<>&,
const enable_shared_from_this<_Tp1>*,
const _Tp2*) noexcept;
mutable weak_ptr<_Tp> _M_weak_this;
};
可以看到:enable_shared_from_this
模板类提供两个public属性的shared_from_this成员函数。这两个函数内部会通过_M_weak_this
成员来创建shared_ptr。其中,_M_weak_assign
函数不能手动调用,这个函数会被shared_ptr自动调用,该函数是用来初始化唯一的成员变量_M_weak_this
。
现在来分析:根据对象生成顺序,先初始化基类enable_shared_from_this,再初始化派生类Frame对象本身。这时Frame对象己经生成,但_M_weak_this成员还未被初始化,最后应通过shared_ptr
等方式调用shared_ptr构造函数(内部会调用_M_weak_assign
)来初始化_M_weak_this
成员。而如果在调用shared_from_this函数之前weak_this_成员未被初始化,则会通过ASSERT报错提示。
更深层次原理:这个enable_shared_from_this中有一个弱指针weak_ptr,这个弱指针能够监视this。在调用shared_from_this这个函数时,这个函数内部实际上是调用weak_ptr的lock方法。lock()会让shared_ptr指针计数+1,同时返回这个shared_ptr,这就是工作原理。