引用计数的存在会带来一些性能影响:
std::shared_ptr的大小是裸指针的两倍。 因为它们内部既包含一个指向资源的裸指针,也包含一个指向该资源的引用计数的裸指针(非标准规定)。- 引用计数的内存必须动态分配。 引用计数与被指向的对象相关联,然而被指向的对象对此却一无所知,因为它们没有存储引用计数的位置。
- 引用计数的递增和递减必须是原子操作。 因为在不同线程中可能存在并发的读写器。原子操作一般都比非原子操作满,所以即使引用计数只有一个字长,也应当假设读写它们的成本是比较高昂的。
std::shared_ptr 也使用 delete 运算符作为其默认资源析构机制,它也支持自定义析构器。对于 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> ,各有一个不同类型的自定义析构器,由于 pw1 和 pw2 具有相同类型,所以可以被放置在元素类型相同的容器中:
auto customDeleter1 = [](Widget* pw) {...};
auto customDeleter2 = [](WIdget* pw) {...}:
std::shared_ptr<Widget> pw1(new Widget, customDeleter1);
std::shared_ptr<Widget> pw2(new Widget, customDeleter2);
std::vector<std::shared_ptr<Widget>> vpw{ pw1, pw2 };std::shared_ptr 自定义析构器不会改变它的大小,无论析构器是怎样的类型,它的大小都相当于裸指针的两倍。
我们可以想想与 std::shared_ptr<T> 对象相关的内存模型:
| 指向T类型对象的指针 | 指向控制块的指针 |
|---|---|
| T类型的对象 | 引用计数 |
| 弱计数 | |
| 其他数据(自定义删除器、分配器等) |
控制块的创建遵循以下规则:
std::make_shared总是创建一个控制块。- 从具备专属所有权的指针(
std::unique_ptr)出发构造一个std::shared_ptr时,会创建一个控制块。 - 当
std::shared_ptr构造函数使用裸指针作为实参来调用时,它会创建一个控制块。
这会导致一个后果:使用同一个裸指针构不止一个 std::shared_ptr 会导致被指向的对象将会有多重控制块。意味着该对象会被析构多次。导致未定义行为。
auto pw = new Widget;
std::shared_ptr<Widget> spw1(pw, loggingDel);
std::shared_ptr<Widget> spw2(pw, loggingDel); // dangerous!如果必须将一个裸指针传递给 std::shared_ptr 构造函数,建议直接传递 new 的结果,而非裸指针变量:
std::shared_ptr<Widget> spw1(new Widget, loggingDel);使用裸指针作为 std::shared_ptr 构造函数实参时,会有一种行为导致 this 指针的多重控制块。假设我们的程序使用 std::shared_ptr 托管 Widget 对象,并用数据结构追踪被处理的 Widget:
std::vector<std::shared_ptr<Widget>> processedWidgets;假设 Widget 有个成员用来做处理:
class Widget {
public:
...
void process();
...
};
void Widget::process() {
...
processedWidgets.emplace_back(this);
// dangerous!
}由此构造的 std::shared_ptr 将为其所指向的 *this 对象创建一个新的控制块,如果该对象事先已经被 std::shared_ptr 拥有,这会导致多重控制块,造成未定义行为。
标准库提供了一种方法可以安全地由 this 指针创建一个 std::shared_ptr :
class Widget: public std::enable_shared_from_this<Widget> {
public:
...
void process();
...
};
void Widget::process() {
...
processedWidgets.emplace_back(shared_from_this());
}从内部实现角度看, shared_from_this 查询当前对象的控制块,并创建一个指向该控制块的新 std::shared_ptr。这样的是西安依赖于当前对象已有一个与其关联的控制块。为了是西安这一点,就必须有一个已经存在的指向当前对象的 std::shared_ptr。如果这样的 std::shared_ptr 不存在,那么该行为未定义,通常抛出异常。
为了避免上述问题,通常将此类的构造函数声明为private,只允许用户通过返回 std::shared_ptr 的工厂函数来创建对象:
class Widget: public std::enable_shared_from_this<Widget> {
public:
template<typename.. Ts>
static std::shared_ptr<Widget> create(Ts&&... params);
...
void process();
...
private:
... // 构造函数
};std::shared_ptr 不能做的事包括数组处理:它并未提供 operator[] 接口,并且禁止 std::unique_ptr<T[]> 向 std::shared_ptr 转换。且无法向 std::unique_ptr 转换。
此外,std::shared_ptr 支持从派生类到基类指针的转换。