本文主要参考《Effective Modern C++》和《Primer C++》 为什么使用shared_ptr 在软件项目中经常碰到的一个头疼问题就是资源泄漏,或者更具体一点说内存泄漏,智能指针就是为了解决这个问题而诞生。 对于面向对象的开发语言而言,我们编写的程序中所有的对象都是有严格定义的生存周期。全局对象在程序启动时分配创建,在程序退出时销毁。对于局部临时对象,当程序进入其定义所在的作用域是被创建,离开时被销毁。局部static对象在第一次使用前分配,程序结束时销毁。对于这几种对象都是自动销毁的。 但是对于动态分配的对象,必须要显式地释放,否则就会造成资源泄漏。动态对象的正确释放是一个极其容易出错的地方。比如动态内存的使用很容易出问题,有时候会忘记释放内存而造成内存泄漏;有时则是还有其它指针引用内存的时候就释放了它,导致其它指针在使用这块内存的时候会出现问题。对于这个问题,标准库提供了智能指针来管理动态分配的对象。当一个对象应该被释放时,指向它的智能指针会自动的释放它。本文主要讲shared_ptr。
shared就是共享的意思,一个通过std::shared_ptr访问的对象其生命周期由指向它的指针们共享所有权(shared ownership)。没有特定的std::shared_ptr拥有该对象。相反,所有指向它的std::shared_ptr都能相互合作确保在它不再使用的那个点进行析构。当最后一个std::shared_ptr到达那个点,std::shared_ptr会销毁它所指向的对象。就内存回收来说,开发者不需要关心指向对象的生命周期,而对象的析构是确定性的。
std::shared_ptr通过引用计数来确保它是否是最后一个指向某种资源的指针,引用计数关联资源并跟踪有多少个std::shared_ptr指向该资源。std::shared_ptr构造函数递增引用计数值,析构函数递减值,拷贝赋值运算符可能递增也可能递减值。(如果sp1和sp2是std::shared_ptr并且指向不同对象,赋值运算符sp1=sp2会使sp1指向sp2指向的对象。直接效果就是sp1引用计数减一,sp2引用计数加一。)如果std::shared_ptr发现引用计数值为零,没有其他std::shared_ptr指向该资源,它就会销毁资源。 引用计数暗示着性能问题: shared_ptr的一些问题
-
std::shared_ptr大小至少是原始指针的两倍,因为它内部包含一个指向资源的原始指针,还至少包含一个资源的引用计数值。shared_ptr的内存结构其实会包含一个控制块,引用计数就是在控制块中。 -
引用计数必须动态分配。 理论上,引用计数与所指对象关联起来,但是被指向的对象不知道这件事情(不知道有指向自己的指针)。因此它们没有办法存放一个引用计数值。但是使用std::make_shared创建std::shared_ptr可以避免引用计数的动态分配,但是还存在一些std::make_shared不能使用的场景,这时候引用计数就会动态分配。 -
由于线程安全问题(多个reader、writer可能在不同的线程),递增递减引用计数必须是原子性的。比如,指向某种资源的std::shared_ptr可能在一个线程执行析构,在另一个不同的线程,std::shared_ptr指向相同的对象,但是执行的确是拷贝操作。原子操作通常比非原子操作要慢,所以即使是引用计数,你也应该假定读写它们是存在开销的。 std::shared_ptr构造函数并不总是递增指向对象的引用计数,因为移动构造函数的存在。从另一个std::shared_ptr移动构造新std::shared_ptr会将原来的std::shared_ptr设置为null,那意味着老的std::shared_ptr不再指向资源,同时新的std::shared_ptr指向资源。这样的结果就是不需要修改引用计数值。因此移动std::shared_ptr会比拷贝它要快:拷贝要求递增引用计数值,移动不需要。移动赋值运算符同理,所以移动赋值运算符也比拷贝赋值运算符快。
|