条款 19 - 使用 std::shared_ptr 管理具备共享所有权的资源

ZaynPei Lv6

不同于std::unique_ptr,std::shared_ptr 是一种用于管理共享所有权资源的智能指针。当多个代码块需要共同拥有和管理同一个对象的生命周期,并且希望在该对象的最后一个使用者结束使用时自动销毁它,std::shared_ptr 就是理想的选择。它实现了类似垃圾回收的自动内存管理,但其析构是确定性的。

工作原理:引用计数

std::shared_ptr 的核心机制是引用计数 (reference counting)。而引用计数的关键在于控制块 (Control Block):每一个由 std::shared_ptr 管理的资源都有一个与之关联的、在堆上分配的“控制块”。这个控制块中包含一个引用计数值,用来跟踪有多少个 std::shared_ptr 正指向该资源 。

当一个新的 std::shared_ptr 通过直接构造, 拷贝构造或拷贝赋值指向一个资源时,其引用计数值会递增(原子操作)。当一个 std::shared_ptr 被析构或被赋值为指向另一个资源时,其原指向资源的引用计数值会递减(原子操作)。

当引用计数值减至 0 时,意味着不再有 std::shared_ptr 指向该资源,最后一个递减计数的 std::shared_ptr 会负责销毁该资源 。

注意, 不能说 std::shared_ptr 的构造函数一定会增加引用计数, 因为从一个已有 std: :shared_ptr 移动构造一个新的std::shared ptr 会将源 std: shared ptr 置空, 结果是不需要进行任何引用计数操作。从这里也可以看出来移动操作要比拷贝操作快不少

性能与内存开销

std::shared_ptr 并非零开销,其成本主要体现在:

  • 尺寸:一个 std::shared_ptr 对象的大小是裸指针的两倍 。一个指针用于指向资源,另一个指针用于指向控制块 。

  • 控制块分配:控制块本身需要在堆上动态分配内存。不过,条款21中介绍的 std::make_shared 可以避免这次额外的分配。

  • 原子操作:引用计数的增减必须是原子操作,因为不同线程可能同时对指向同一资源的 std::shared_ptr 进行操作。原子操作通常比非原子操作要慢 。

自定义删除器

与 std::unique_ptr 类似,std::shared_ptr 也支持自定义删除器。但两者之间存在一个关键区别:std::shared_ptr 的删除器类型不是其类型的一部分 。

这意味着不同删除器的 std::shared_ptr 具有完全相同的类型。这使得它们可以被存储在同一个容器中(例如 std::vector<std::shared_ptr>),提供了比 std::unique_ptr 更高的灵活性 。

另一点不同,是自定义析构器不会改变 std::shared_ptr 的尺寸。无论析构器是怎样的型别,std::shared_ptr 对象的尺寸都相当于裸指针的两倍. 这是因为自定义删除器被存储在堆上的控制块中,而不是 std::shared_ptr 对象本身内部。更进一步, std::shared_ptr 的第二个指针所指的控制块包含下列许多信息:

一个对象的控制块由创建首个指涉到该对象的 std::shared_ptr 来确定。此外, 控制块的基本规则如下: - std::make_shared(参见条款 21)总是创建一个控制块。std::make_shared 会生成一个用于管理所涉及新对象的控制块,因为在调用 std::make_shared 的时刻,显然不会有针对该对象的控制块存在。

  • 从具备专属所有权的指针(即 std::unique_ptr 或 std::auto_ptr 指针)出发构造一个 std::shared_ptr 会创建一个控制块。专属所有权指针不使用控制块,因此对于其所指涉的对象来说,不应存在控制块。作为构造过程的一部分,std::shared_ptr 被指定了其所指涉对象的所有权,因此那个专属所有权的智能指针会被置空。

  • 当 std::shared_ptr 的构造函数使用裸指针作为实参调用时,它会创建一个控制块。这个看起来平平无奇的点会造成一种致命的错误。例如:

    1
    2
    3
    4
    5
    auto pw = new Widget; // pw 是一个裸指针

    // 错误!为同一个裸指针创建了两个控制块
    std::shared_ptr<Widget> spw1(pw); // 为 *pw 创建一个控制块,引用计数为 1
    std::shared_ptr<Widget> spw2(pw); // 也为 *pw 创建一个控制块,引用计数为 1
    spw1 和 spw2 各自创建了一个独立的控制块。它们都认为自己是 *pw 的唯一所有者群体。当 spw1 被销毁时,它会发现其引用计数变为0,于是它会 delete pw。之后,当 spw2 被销毁时,它也会发现其引用计数变为0,于是它会再次 delete pw。对同一个指针进行两次删除会导致未定义行为。

因此, 永远不要用同一个裸指针来初始化多个 std::shared_ptr, 而是应该通过复制一个已存在的 std::shared_ptr 来创建新的 std::shared_ptr,以确保它们共享同一个控制块。

this 指针问题与 std::enable_shared_from_this

On this page
条款 19 - 使用 std::shared_ptr 管理具备共享所有权的资源