我应该在 class 构造函数内部还是外部初始化 shared_ptr ?

Should I initialize a shared_ptr inside or outside the class constructor?

给定以下示例代码:

#include <memory>

class Foo {
public:
    Foo(std::shared_ptr<int> p);
private:
    std::shared_ptr<int> ptr;
};

Foo::Foo(std::shared_ptr<int> p) : ptr(std::move(p)) {
}

class Bar {
public:
    Bar(int &p);
private:
    std::shared_ptr<int> ptr;
};

Bar::Bar(int &p) : ptr(std::make_shared<int>(p)) {
}

int main() {
    Foo foo(std::make_shared<int>(int(256)));
    Bar bar(*new int(512));

    return 0;
}

Foo 和 Bar 都可以正常工作。但是,在调用构造函数时创建 shared_ptr 然后使用 std::move 转移所有权和仅传递对对象的引用并将 shared_ptr 的创建委托给 class 构造函数?

我认为第二种方法更好,因为我不必移动指针。但是,我在阅读的代码中看到的大多是第一种方式。

我应该使用哪一个,为什么?

Foo 是正确的。

酒吧令人厌恶。它涉及内存泄漏、不安全的异常行为和不必要的复制。

编辑:内存泄漏的解释。

解构这一行:

Bar bar(*new int(512));

这些操作的结果:

  1. 调用 new int(512),调用 operator new,返回指向堆上 int 的指针(内存分配)。
  2. 解引用指针以便为 Bar 的构造函数提供常量引用
  3. Bar 然后用 make_shared 返回的一个构建它的 shared_ptr(这部分是有效的)。这个 shared_ptr 的 int 是用引用传递的 int 的副本初始化的。
  4. 然后是函数returns,但是由于没有变量记录new返回的指针,所以没有什么可以释放内存。为了销毁对象并释放其内存,每个新的都必须通过删除进行镜像。
  5. 因此内存泄漏

这取决于你想要达到什么。

如果你在内部需要一个 shared_ptr,因为你想与你稍后创建的其他一些对象共享这个对象,第二种方法可能更好(除了那个可怕的构造函数调用显然)。

如果您想要共享现有对象的所有权(这是更常见的情况,真的),您别无选择,您需要使用第一种方式。

如果这些都不适用,您可能一开始就不需要 shared_ptr

第二种方式不正确;它泄漏内存。有

Bar::Bar(int &p) : ptr(std::make_shared<int>(p)) {
}

....

Bar bar(*new int(512));

std::make_shared做的shared_ptrp的值(也就是512)构建一个新的共享指针,负责新的一块内存;它不对 p 所在的内存地址负责。这块——你在 main 中分配的那块——然后就丢失了。这段特殊的代码适用于

                        // +---------------------- building a shared_ptr directly
                        // v                v----- from p's address
Bar::Bar(int &p) : ptr(std::shared_ptr<int>(&p)) {

...但是看看那个。 那是纯粹的邪恶。没有人期望构造函数的引用参数要求它引用新对象将负责的堆对象。

你可以更理智地写

Bar::Bar(int *p) : ptr(p) {
}

....

Bar bar(new int(512));

事实上,如果您愿意,您可以为 Foo 提供第二个构造函数来执行此操作。函数签名使指针必须指向堆分配的对象有多清晰,这有点争论,但 std::shared_ptr 提供了相同的功能,因此有先例。这是否是个好主意取决于您的 class 做什么。

两者都可以正常工作,但是您在main中使用它们的方式不一致。

当我看到构造函数获取引用时(如 Bar(int& p)),我希望 Bar 持有引用。当我看到 Bar(const int& p) 时,我希望它能保存一份副本。 当我看到一个右值引用(不是通用的,比如 Bar(int&& p) 我希望 p 在通过后不是 "surviving its content"。(好吧......对于一个没有那么有意义的......)。

在任何情况下 p 持有 int,而不是 pointer,并且 make_shared 期望的是转发到 int 构造函数的参数(即 . .. 一个整数,而不是整数 *)。

你的主所以必须是

Foo foo(std::make_shared<int>(int(256)));
Bar bar(512);

这将使 bar 保存值 512 的动态分配的可共享副本。

如果你这样做 Bar bar(*new int(512)) 你会创建 bar 来保存你的 "new int" 的副本,它的指针被丢弃,因此 int 本身泄漏。

一般来说,像 *new something 这样的表达应该读作 "no no no no no ..."

但是你的 Bar 构造函数有一个问题:要能够接受常量或表达式返回值,它必须接受 const int&,而不是 int&

如果您打算独占堆分配对象的所有权,我建议您接受 std::unique_ptr。它清楚地记录了意图,如果需要,您可以在内部从 std::unique_ptr 创建一个 std::shared_ptr

#include <memory>

class Foo {
public:
    Foo(std::unique_ptr<int> p);
private:
    std::shared_ptr<int> ptr;
};

Foo::Foo(std::unique_ptr<int> p) : ptr(std::move(p)) {
}

int main() {
    Foo foo(std::make_unique<int>(512));
}

不要这样做 Bar,它很容易出错并且无法描述您的意图(如果我正确理解您的意图)。

假设您使用 int 只是作为示例,并且有一个真实的资源,那么这取决于您想要实现什么。

第一种是典型的依赖注入,通过构造函数注入对象。好的一面是可以简化单元测试。

第二种情况只是在构造函数中创建对象,并使用通过构造函数传递的值对其进行初始化。


顺便说一下,请注意您的初始化方式。这 :

Bar bar(*new int(512));

导致内存泄漏。已分配内存,但从未解除分配。