【问题标题】:Understanding c++ shared pointers理解 C++ 共享指针
【发布时间】:2016-05-09 15:38:15
【问题描述】:

您好,我正在制作自己的引用计数智能指针,但在开始之前有两个概念我不太了解。

  1. 我知道当创建一个共享指针指向一个对象时,我必须为一个结构/类分配内存,该结构/类将包含诸如引用计数(最初为一个)之类的信息,也许还有一个互斥体用于递增和递减。当我使用 =operator 使另一个共享指针也指向这个对象时,我还将指向这个结构/类的指针传递给那个新指针,这样我就可以增加计数。我的问题是,如果我将第三个共享指针指向该对象(不使用复制构造函数或 =operator),那么该指针将不知道该结构,因此如果我删除,则引用计数为 1指针,计数将达到 0,对象将被销毁,而实际上,该对象还有另外两个指针?

  2. 如果一个共享指针的引用计数为 1,然后创建了多个线程,如果一个线程结束/销毁它,其他可能仍在运行的线程会发生什么?

    李>

【问题讨论】:

  • 对于 (1),您的意思是:shared_ptr<T> foo(new T); shared_ptr<T> bar = foo; shared_ptr<T> quux(foo.get());?
  • 编写共享指针不是一个初学者项目。这就是为什么我们有标准版本。我写了一篇关于人们在实现智能指针时常犯的错误的博客文章:Loki Astari's blog 另外,当你完成后,值得在Code Review进行审查。
  • 为什么?如果是为了学习如何做,那么好吧。否则,只需使用 std::unique_ptrstd::shared_ptr - 不要重新发明轮子。

标签: c++ pointers shared-ptr smart-pointers


【解决方案1】:

我知道,当创建一个共享指针指向一个对象时,我必须为一个结构/类分配内存,该结构/类将包含诸如引用计数(最初为一个)之类的信息,也许还有一个互斥体,用于递增和递减。

是的,您需要一个柜台。
仅当您计划多线程时才需要互斥锁。我会专注于让计数工作首先担心锁定后记。

当我使用 =operator 来使另一个共享指针也指向这个对象时,我还将指向这个结构/类的指针传递给那个新指针,这样我就可以增加计数。

关于共享指针的要点是它们拥有指针的所有权。一旦创建了共享指针,就不应该有指向同一对象的 RAW 指针实例。因此,当您复制或分配时,您都是通过共享指针完成的。

我的问题是,如果我将第三个共享指针指向该对象(不使用复制构造函数或 =operator),那么该指针将不知道该结构,因此引用计数为 1。

你的假设是正确的。这就是为什么在创建共享指针时不应保留指针的副本。这是引入std::make_shared() 的另一个原因,它分配内存并立即将其包装在智能指针中,因此不会将原始指针返回给用户代码。

如果我然后删除指针,计数将达到 0,对象将被销毁,而事实上,该对象还有另外两个指针?

这正是问题所在。这就是为什么您不应该创建或传递指向已经被管理的对象的 RAW 指针。

如果共享指针的引用计数为 1,然后创建了多个线程,如果一个线程结束/销毁它,其他可能仍在运行的线程会发生什么情况?

如果您的引用计数正常,并且您只有共享指针来管理 RAW 指针,那么它应该可以按预期工作。但是如果你在一个线程中删除它,它会在所有线程中被销毁。

【讨论】:

  • 这与make_shared()无关。我可以随时致电get() 并将其包装在新的shared_ptr...
  • @Barry make_shared 是相关的,因为它是一种经常用于防止像 OP 所描述的问题的工具。这非常相关。
  • @Xirema 不,不是。 make_shared 的存在是为了将控制块与对象一起分配 - 节省一次分配。这并不妨碍我写auto p = std::make_shared<int>(42); std::shared_ptr<int> q(p.get());
  • @Barry:当然。但我在想。 sharead_ptr<X> anObj(p); p 可以分配到包装以外的其他地方(这就是 OP 的看法)。使用 make_shared() 可以解决这个问题,并可能提高效率。
  • @Barry 但是它确实阻止了int * p = new int(10); shared_pointer<int> s1 = p; shared_pointer<int> s2 = p;,当它超出范围时会导致UB。
【解决方案2】:

我的问题是,如果我让第三个共享指针指向这个对象(不使用复制构造函数或 =operator)

由于您绕过复制共享指针的机制,这将是未定义的行为。使用复制构造函数或赋值运算符。

如果共享指针的引用计数为 1,然后创建了多个线程,如果一个线程结束/销毁它,其他可能仍在运行的线程会发生什么情况?

这意味着您在其析构函数完成后使用共享指针,这是未定义的行为。确保这些线程拥有自己的共享指针副本以避免这种情况,或者确保指针在其他线程使用它时不会被破坏。

【讨论】:

    【解决方案3】:

    我的问题是,如果我让第三个共享指针指向这个对象(不使用复制构造函数或 =operator),那么这个指针将不知道该结构,因此引用计数为 1,如果然后我删除指针,计数会达到0,对象会被销毁,其实这个对象还有另外两个指针?

    你真的无能为力。如果您有一个指针并将其分配给两个单独的共享指针,那么您将获得双重删除。

    int * foo = new int(10); // set value to 10
    {
        std::shared_ptr<int> a(foo);
        std::shared_ptr<int> b(foo)
    } // uh-oh.  we call delete twice
    

    无法将指针标记为已拥有。由编写代码的人决定不这样做。我们解决此类问题的方法是直接在构造函数中创建指针,这样您就不能将其提供给另一个实例

    std::shared_ptr<int> a(new int(10));
    

    或者你可以使用std::make_shared

    auto b = std::make_shared<int>(10);
    

    现在我们不再需要担心将原始指针提供给另一个共享指针并将其从我们下面删除。

    如果共享指针的引用计数为 1,然后创建了多个线程,如果一个线程结束/销毁它,其他可能仍在运行的线程会发生什么情况?

    如果您将共享指针传递给每个线程,那么它们都共享所有权。在所有线程结束并且原始共享指针超出范围之前,该指针不会被删除。

    【讨论】:

    • 一个讨厌的、狡猾的人仍然可以在共享指针上调用.get() 并从中构造一个新指针。但是,正如你所说,对此无能为力,无论谁这样做,他们都应该承受所有的痛苦。
    【解决方案4】:
    1. 答案是肯定的。如果您从指向对象的普通指针创建共享指针,当该对象已被另一个共享指针拥有时,则当一个引用计数达到 0 时该对象将被销毁,并且剩余的共享指针将悬空 - 它们一直指向对象曾经存在的地方。

      这就是为什么智能指针的用户绝不能从本身没有所有权的代码中授予指针的所有权。

    2. 检查和递减引用计数器存在竞争条件。如果共享指针可以在多个线程中使用,那么对引用计数器的访问必须跨线程同步。

    【讨论】:

      【解决方案5】:

      在一个典型的实现中,如果你为一个对象创建一个shared_pointer,并为同一个对象创建第二个shared_pointer而不引用已经创建的shared_pointer,它们的引用计数都是1,当任何一方超出范围时删除该对象,并在另一方尝试删除该对象时导致未定义的行为。

      这是 STL 具有 std::make_sharedstd::make_unique 等工厂对象的主要原因之一,因为确保程序员永远不会管理原始对象指针有助于防止此类错误。

      当然,更智能的实现可能会尝试在静态级别上跟踪所有已创建和维护的指针,并可能尝试记忆对象。但这有其自身的风险和问题,所以我不建议这样做。

      至于多线程问题:这取决于。如果所有线程都基于原始shared_pointer 对象创建shared_pointer,并且您正确使用mutex 来维护引用计数,那么它们都应该正确地增加引用计数而不会出现问题,并且对象应该'不会因为一个线程停止使用它而被删除。

      【讨论】:

      • 如果我要投反对票,你至少能告诉我我的回答有什么问题吗?
      猜你喜欢
      • 1970-01-01
      • 2017-01-09
      • 1970-01-01
      • 2017-05-14
      • 2015-02-10
      • 1970-01-01
      • 1970-01-01
      • 2012-01-11
      • 2019-07-06
      相关资源
      最近更新 更多