std::shared_ptr
可以用过访问资源的引用计数来确定是否自己是最后一个指涉到该资源的。std::shared_ptr
的构造函数会使改计数递增,而析构函数会使该计数递减,而赋值运算符同时执行两种操作。
引用计数也会带来一些性能的影响:
-
std:;shared_ptr
的尺寸是裸指针的两陪。因为内部既包含一个指涉到该资源的裸指针,也包含一个指涉到该资源的引用计数的裸指针 - 引用计数的内存必须动态分配
- 引用计数的递增和递减必须是原子操作
std::shared_ptr
也使用delete
运算符作为其默认资源析构机制,同样也支持自定义析构器。然而这种支持的设计却与std::unique_ptr
有所不同。对于std::unique_ptr
而言,析构器的类型是智能指针一部分。但对于std::shared_ptr
而言,却并非如此:
auto loggingDel = [](Widget *pw) { // 自定义析构器
makeLogEntry(pw);
delete pw;
};
std::unique_ptr<Widget, decltype(loggingDel)> upw(new Widget, loggingDel); // 析构器类型为智能指针类型的一部分
std::shared_ptr<Widget> spw(new Widget, loggingDel); // 析构器类型不是智能指针类型的一部分
考虑两个std::shared_ptr<Widget>
,各有一个不同类型的自定义析构器:
auto customDelete1 = [](Widget *pw){...}; // 自定义析构器
auto customDelete2 = [](Widget *pw){...}; // 各有不同类型
std::shared_ptr<Widget> pwd1(new Widget, customDeleter1);
std::shared_ptr<Widget> pwd1(new Widget, customDeleter2);
由于pw1和pw2具有同一类型,因此它们可以放置在元素类型为该对象类型的容器中:
std::vector<std::shared_ptr<Widget>> vpw{pw1, pw2};
它们也可以相互赋值,并且都可以被传递至要求std::shared_ptr<Widget>
类型形参的函数。并且都可以被传递然而对于具有不同定义析构器类型的std::unique_ptr
来说,以上均无法实现,因为自定义析狗器的类型会影响std::unique_ptr
的类型;
自定义析构器不会改变std::shared_ptr
的尺寸。自定义析构器有可能是函数对象,而对象可以包含任意数量的数据,这意味着它们的尺寸可能是任意大小。这并无可能,std::shared_ptr
不得不使用更过内存。然而,该部分内存却不属于std::shared_ptr
对象的一部分。
一个对象的控制块由首个指涉到该对象的std::shared_ptr
的函数来确定。因此,控制块的创建遵循了如下规则:
std::make_shared
总是创建一个控制块- 从具备专属权的指针(
std::unique_ptr
或std::auto_ptr
指针)出发构造一个std::shared_ptr
时,会创建一个控制块 - 当
std:shared_ptr
构造函数使用裸指针作为实参来调用时,它会创建一个控制块
这些规则会导致一个后果:从一个裸指针出发来构造不止一个std::shared_ptr
的话,会产生未定义行为,如下代码:
auto pw = new Widget; // pw是一个裸指针
std::shared_ptr<Widget> spw1(pw, loggingDel); // 为*pw创建一个控制块
std::shared_ptr<Widget> spw2(pw, loggingDel); // 为*pw创建第二个控制块
spw1的构造函数调用时传入一个裸指针,因此它为所指涉到的对象创建一个控制块。但是spw2的构造函数在调用时传入了同一个裸指针,它也为*pw创建了一个控制块。这么一来就会析构两次。
我们从上述代码得出两个结论:
- 尽可能避免將裸指针传递给一个
std::shared_ptr
的构造函数,常用替代手法是使用std::make_shared
- 如果必须將一个指针传递给
std::shared_ptr
的构造函数,就直接传递new
运算符的结果,而非传递一个裸指针变量