两种创建方式
std::shared_ptr<Object> op1(new Object(10));
std::shared_ptr<Obejct> op2 = std::make_shared<Object>(20);
这样两种不同的智能指针构建方式,其内存结构是不同的
对于第一种方式来说,op1有一个ptr指针和一个删除器(mDeletor),ptr指针指向引用计数对象(RefCnt),引用计数对象有mptr指针和引用计数(ref),mptr直指向Object对象,Object对象的value域的值为10,在这个过程中在堆区的构建new了两次,分别是创建引用计数对象与Object对象
对于第二种方式来说进行了一些的优化,它能够计算出Object的大小和引用计数对象的大小,直接一次开辟够Object对象和引用计数大小一样的空间,引用计数对象的mptr指针指向Object对象,在这个过程中在堆区的构建只new一次
make_shared的好处
- make_shared 最大的好处就是减少单次内存分配的次数,这几乎就是我们使用make_shared的唯一理由
- 另一个好处就是可以增大Cache局部性:使用make_shared,计数器的内存和原生内存就在堆上排排坐,这样的话我们所有要访问的两个内存的操作就会比另一种方案减少一半的cache misses
引入Cache的理论基础是程序局部性原理,包括时间局部性和空间局部性;即最近被CPU访问的数据,短期内CPU还要访问(时间);被CPU访问的数据附近的数据,CPU短期内还要访问(空间);因此如果刚刚访问过的数据缓存在Cache中,那下次访问时,可以直接从Cache中取,其速度可以得到数量级的提高,CPU要访问的数据在Cache中有缓存,称为“命中”(Hit),反之称为“缺失”(Miss)
构建方式不同造成的安全问题
class Object
{
int value;
public:
Object(int x = 0) :value(x)
{
cout << "Create Object:" << this << endl;
}
~Object()
{
cout << "Destory Object:" << this << endl;
}
int& Value()
{
return value;
}
};
double couldThrowException() //会抛出异常
{
throw int(10);
return 12.23;
}
void doSomething( double d,std::shared_ptr<Object> pt)
{
}
void fun()
{
try
{
doSomething(couldThrowException(), std::shared_ptr<Object>(new Object(10)));
}
catch (...)
{
cout << "catch(...)" << endl;
}
}
int main()
{
fun();
return 0;
}
上面代码在运行中,由于调用doSomething()
,首先构建Object对象入栈,接着将couldThrowException()
对象返回值入栈,但是由于该函数会抛出异常,并且智能指针并没有进行创建对Object对象进行管理,那么会造成Object构造而没有析构,造成内存泄漏
但是若使用make_shared<>
进行创建智能指针
doSomething(couldThrowException(), std::make_shared<Object>(10));
那么此处仅会抛出异常,并不会构建对象,Object对象的构建会与其智能指针引用计数同时创建
make_shared的坏处
- 使用make_shred,首先最可能遇到的问题就是make_shared函数必须能够调用目标类型构造函数或构造方法;然而这个时候即使把make_shared设成类的友元恐怕都不够用,因为其实目标类型的构造是通过一个辅助函数调用的——不是make_shared这个函数
- 另一个问题就是我们目标内存的生存周期问题(不是目标对象的生存周期);正如上边所说过的,即使被shared_ptr管理的目标对象都被释放了,shared_ptr的计数器还会一直持续存在,直到最后一个指向目标内存的weak_ptr被销毁,这个时候,如果我们使用make_shared函数,问题就来了;程序自动地把被管理对象占用的内存和计数器占用的堆上内存视作一个整体来管理,这就意味着,即使被管理的对象被析构了,空间还在,内存可能并没有归还——它还等着所有的weak_ptr都被清理后和计数器所占的内存一起被归还;假如你的对象有点大,那就意味着一个相当可观的对象被无意义地锁了一段时间