最近在看opencv的文档时,对cv::Ptr类了解了一些,其与STL中的shared_ptr类似,是一种智能指针,于是又看了《C++标准库》中有关智能指针的章节和《Effective Modern C++》中的有关智能指针的章节,将智能指针所涉及的一些内容总结如下。
STL中智能指针
C++中比较棘手的一个问题就是内存的管理,指针在其中扮演了及其重要的作用,但是同时对指针的不当使用会造成很多问题。自C++11起,STL提供了两大类型的智能指针,使得对指针的操作更加安全。其中shared_ptr类实现了共享式拥有概念,而unique_ptr实现了独占式拥有概念。另外还有weak_ptr等辅助类。所有这些指针类都定义在头文件 < memory>中。
shared_ptr
shared_ptr主要使用一种称为引用计数的方法来管理其所指向的内容。当该指针多指向一个内容时,引用计数加一,少指向一个内容时,引用计数值减1,当引用计数值减到0时,该内容会被自动删除。
shared_ptr的声明和初始化
1、使用构造函数初始化
2、新式初始化语法
3、使用make_shared()初始化
4、先声明shared pointer,然后使用reset()方法赋值
注意不能使用赋值符进行初始化
最常用和可靠的初始化方法是使用make_shared()函数。
析构策略
实际上,我们可以声明特化的deleter,可以通过传递一个lambda作为shared_ptr构造函数的第二实参。
注意:shared_ptr提供的default deleter 调用的是 delete,而不是delete [],所以即便为array建立一个shared_ptr是可以的,但是却有致命错误,解决方法是为其指定delete []作为析构函数,最好不要这样使用shared_ptr。
如果析构时不仅仅需要删除内存,还需要 一些其它操作,那么必须指定deleter
shared_ptr陷阱
1、cyclic reference问题
两个对象使用shared_ptr互相指向对方时,使用shared_ptr无法释放相应的资源,因为每个对象的use_count()至少为1。
解决方法:1、使用普通指针,但是这样又得自行管理资源释放,比较麻烦也更容易出问题。2、其中一方的指向使用weak_ptr,可以完美解决该问题。
2、试图访问已被释放的数据
也可通过使用weak_ptr解决。
3、应该确保某对象只被一组shared_ptr拥有
当sp1和sp2丢失p的拥有权时,两者都会试图释放p的资源,即释放资源的操作会执行两次。解决方法是在创建对象和其相应资源的时候直接设立shared_ptr。
weak_ptr
weak_ptr是辅助shared_ptr的类,其用来共享但是不拥有对象。
unique_ptr
概念
unique_ptr实现了独占式拥有概念,其可以确保一个对象和其相应的资源在同一时间只能被一个pointer拥有。unique_ptr是其所指向的对象的唯一拥有者。unique_ptr有着与普通指针相同的接口,不同的是:它不提供pointer算术比如++等操作,但是实际上这省去了一些麻烦。
转移unique_ptr的拥有权
源头和去处
函数可以利用unique_ptr的转移所有权的功能将拥有权转给其他函数。
unique_ptr当做当做class内的成员
可以避免资源泄漏。
Effective Modern C++ 中的有关条款
《Effective Modern C++》一书中有几个关于smart pointer的条款。
Item 18: Use std::unique_ptr for exclusive-ownership resource management
要点:详见原书 P118
1、std::unique_ptr是一种足够小,很快并且只支持move的智能指针,其用来管理独占式资源。默认情况下,std::unique_ptr的大小和raw pointer 是一样的,而且对于大多数包括解引用在内的操作,其都和raw pointer一致。
2、默认情况下,其使用delete作为deleter。但是当deleter采用自定义时,std::unique_ptr的大小会变大,其具体大小取决于函数对象的状态。使用lambda表达式作为deleter是比较好的一种选择。
3、std::unique_ptr不能进行复制,否则就违反了其只能拥有一个对象的规则。
4、std::unique_ptr有两种形式: std::unique_ptr
5、从std::unique_ptr转型为std::shared_ptr很方便,在不知道指针的具体用途时最好先用unique_ptr,待需要时再转型为shared_ptr。
题外话要想正确调用继承类的析构函数,基类的析构函数必须是虚函数。
Item 19: Use std::shared_ptr for shared_ownership resource management
要点:详见原书 P125
1、关于引用计数
通常,构造函数增加引用计数;析构函数减少引用计数;复制赋值操作减少左边所指对象的引用计数,增加右边所指对象的引用计数。
2、引用计数会影响性能
主要由以下原因造成:首先,shared_ptr的大小是原始指针的2倍,因为其有1个指针指向所引的对象,1个指针指向引用计数;然后,用于引用计数的内存要动态分配;对于引用计数的增加和减少操作必须是原子性的,这是为了保证其在多线程情况下的安全性。
3、shared_ptr和unique_ptr的区别和联系
A、都支持自定义deleter,但是实现有所不同,对于unique_ptr,deleter的类型是智能指针类型的一部分,而对于shared_ptr,deleter的类型不是其一部分。
所以,对于不同类型的deleter的两个shared_ptr,只要它们的对象类型相同,其就可以相互赋值;而对于unique_ptr则不能,因为deleter也是其类型的一部分,这意味着这两个unique_ptr的类型不同,不能相互转移所有权。
B、对于shared_ptr,自定义deleter并不改变shared_ptr的大小,但是对于unique_ptr是有影响的。
C、shared_ptr不支持数组类型。
4、shared_ptr的实现原理
其使用control block管理引用计数及其他(包括自定义deleter)数据。
我们希望某个对象的control block是在初次有指针指该对象时创建。然而当创建shared_ptr指向某一对象时,我们事先并不能知道是否已有其它shared_ptr指向该对象。于是使用以下规则来进行control block的创建:
std::make_shared总是创建一个control block
当shared_ptr是由unique_ptr转型创建时要建立control block_
当用raw pointer构造shared_ptr时要创建control block
考虑如下语句
按照之前的control block创建规则,对于pt指针指向的对象,将会创建2个control block,于是离开作用域时,会尝试对*pt进行两次内存释放操作,这样第二次操作将会造成未定义行为。这是我们不愿意看到的。
从以上例子我们可以得到这样的经验:
避免向shared_ptr的构造函数传递raw pointer
可以采用以下方法
5、可使用std::enable_shared_from_this将this指针转型成shared_ptr
Item 20: Use std::weak_ptr for std::shared_ptr like pointers that can dangle
要点:具体看原书P134
std::weak_ptr不会影响引用计数值
std::weak_ptr的行为和shared_ptr相似,但是其不会影响所指对象的引用计数值,也不能被解引用,其是shared_ptr的辅助。
一个问题就是:在判断weak_ptr仍然指着一个特定对象时,如何访问该对象?这里有几个棘手的问题需要解决:weak_ptr并没有解引用操作,而且即便有解引用操作,在判断是否expired和解引用的语句之间,可能有另一个线程使得最后一个指向该对象的指针失效,使得该对象被destroy,这样解引用操作将会导致未定义行为,即我们无法保证线程安全性。
我们需要的是一个原子性的操作检查weak_ptr是否expired,如果没有,那么访问该指针所指对象,这可以通过从weak_ptr构造一个shared_ptr实现。
2、weak_ptr的应用场景
A、管理缓存时,需要知道cache什么时候会dangle,这样的场景就需要weak_ptr辅助。(P136)
B、解决shared_ptr的环形引用问题。
3、weak_ptr和shared_ptr
从效率的角度讲,weak_ptr的效率和shared_ptr是一样的。其存储和shared_ptr是一样的 ,其大小和shared_ptr也是一样的,它也要使用control block管理对象。注意:weak_ptr不是不管理引用计数,而是不拥有对象共享关系,所以不会影响所指对象的引用计数,但是其会管理另外的不同功能的引用计数。
Item 21: Prefer std::make_unique and std::make_shared to direct use of new
要点:具体看原书P139
1、使用make函数的优点
make_shared是C++11的标准,make_unique是C++14的标准。
A、使得代码和编译的目标代码更精简
B、使用make函数使得异常安全(exception safety)
C、make_shared和allocate_shared函数使得编译器生成更小更快的代码(相比使用new创建)
原则是优先使用make函数创建只能指针,但是也有make函数不能使用的情况。
A、比如要自定义deleter时,就只能使用new作为参数创建,而不能使用make函数。
B、一些所指对象的初始化细节使得不能使用make函数创建智能指针。
C、一些自定义了new和delete的对象不方便用make函数创建智能指针。
D、对于比较大的对象,不要使用make函数创建智能指针。
Item 22: When using the Pimpl Idiom, define special member functions in the implementation file
要点:
1、Pimpl Idiom通过类客户和类实现之间减少编译依赖来减少构建时间
2、对于std::unique_ptr pImpl指针,需要在类头文件中声明特殊成员函数,在实现文件中实现。即便是默认函数实现,也需要声明和定义。
3、对于 shared_ptr则没有以上这种限制。主要原因还是在于unique_ptr和shared_ptr中deleter是否是类型的一部分的一个区别。
opencv 中的cv::Ptr
其行为和std::shared_ptr类似。用法也基本相同。
参考
1、Nicolai M. Josuttis
The C++ Standard Library - A Tutorial and Reference, 2nd Edition
Addison Wesley Longman, 2012
2、Scott Meyers
Effective Modern C++ : 42 Specific Ways to Improve Your Use of C++11 and C++14
3、std::shared_ptr
4、std::weak_ptr
5、std::unique_ptr
6、cv::Ptr