使用智能指针管理类成员变量

185

我在理解C++11中将智能指针作为类成员的使用方面遇到了困难。我已经阅读了很多有关智能指针的内容,并且我认为我基本上了解了unique_ptrshared_ptr/weak_ptr的工作原理。但我不明白的是真正的用法。似乎每个人都建议几乎始终使用unique_ptr。但是,我该如何实现这样的东西:

class Device {
};

class Settings {
    Device *device;
public:
    Settings(Device *device) {
        this->device = device;
    }

    Device *getDevice() {
        return device;
    }
};    

int main() {
    Device *device = new Device();
    Settings settings(device);
    // ...
    Device *myDevice = settings.getDevice();
    // do something with myDevice...
}

假设我想要用智能指针替换指针。因为有getDevice(),所以unique_ptr行不通,对吧?这时候我就要使用shared_ptrweak_ptr了?没有办法使用unique_ptr吗?在我看来,除非在非常小的范围内使用指针,否则shared_ptr在大多数情况下更合适。

class Device {
};

class Settings {
    std::shared_ptr<Device> device;
public:
    Settings(std::shared_ptr<Device> device) {
        this->device = device;
    }

    std::weak_ptr<Device> getDevice() {
        return device;
    }
};

int main() {
    std::shared_ptr<Device> device(new Device());
    Settings settings(device);
    // ...
    std::weak_ptr<Device> myDevice = settings.getDevice();
    // do something with myDevice...
}

那是正确的方式吗?非常感谢!


7
清楚了解生命周期、所有权和可能出现的空值非常重要。例如,在将 device 传递给 settings 的构造函数后,您是否仍然希望能够在调用范围内引用它,还是只能通过 settings 引用?如果是后者,那么使用 unique_ptr 很有用。另外,您是否有一种情况,其中 getDevice() 的返回值为 null。如果没有,则只需返回一个引用即可。 - Keith
2
是的,在10个场景中,8个场景使用shared_ptr是正确的。另外的2个场景则需要使用unique_ptrweak_ptr。此外,weak_ptr通常用于打破循环引用;我不确定你的用法是否正确。 - Collin Dauphinee
2
首先,你想要device数据成员的所有权是什么?你必须先决定这个。 - juanchopanza
2
好的,我明白了,作为调用者,如果我知道现在不再需要它,我可以使用unique_ptr并在调用构造函数时放弃所有权。但是作为Settings类的设计者,我不知道调用者是否也想保留引用。也许设备将在许多地方使用。好的,也许这正是你的观点。在那种情况下,我将不是唯一的所有者,这就是我想使用shared_ptr的时候吧。还有:所以智能指针确实替换了指针,但不是引用,对吗? - michaelk
this->device = device; 同时使用初始化列表。 - Nils
正如我在已接受的答案中所述,将unique_ptr作为成员变量有许多隐含的限制,其中可能最重要的是标准复制构造函数的隐式删除。这种删除可能会使您的类对任何下游程序员来说极不直观。请参见: https://dev59.com/yGQo5IYBdhLWcg3wdPO8 - ldog
2个回答

231

unique_ptr不能用于getDevice(),对吗?

不一定。这里重要的是确定适当的Device对象所有权策略,即指针(智能指针)指向的对象将由谁拥有。

Settings对象实例独自拥有它吗?当Setting对象被销毁时,Device对象是否需要自动销毁,还是应该比该对象存在更久时间?

在第一种情况下,std::unique_ptr是您所需的,因为它使Settings成为指向对象的唯一(独特)所有者,并且是负责其销毁的唯一对象。

在此前提下,getDevice()应返回一个简单的观察指针(观察指针是不保持所指对象存活的指针)。最简单的观察指针是原始指针:

#include <memory>

class Device {
};

class Settings {
    std::unique_ptr<Device> device;
public:
    Settings(std::unique_ptr<Device> d) {
        device = std::move(d);
    }

    Device* getDevice() {
        return device.get();
    }
};

int main() {
    std::unique_ptr<Device> device(new Device());
    Settings settings(std::move(device));
    // ...
    Device *myDevice = settings.getDevice();
    // do something with myDevice...
}

[注意 1:你可能会想知道为什么我在这里使用裸指针,当大家都在说裸指针是不好的、不安全的和危险的。实际上,这是一个宝贵的警告,但重要的是将其放在正确的背景下:当使用裸指针进行手动内存管理,即通过newdelete分配和释放对象时,裸指针是不好的。当纯粹用作实现引用语义和传递非拥有、观察指针的手段时,裸指针本质上没有什么危险,除了可能需要注意不要解引用悬空指针。 - END 注意 1]

[注意 2:正如评论中所提到的,在这种特殊情况下,当所有权是唯一的且所拥有的对象始终保证存在(即内部数据成员device永远不会是nullptr)时,函数getDevice()可以(甚至应该)返回一个引用而不是指针。虽然这是正确的,但我在这里决定返回一个裸指针,因为我希望这是一个简短的答案,可以推广到device可能为nullptr的情况,并展示裸指针只要不用于手动内存管理是可以使用的。 - END 注意 2]


当然,如果你的Settings对象不应该独占设备,则情况完全不同。例如,如果Settings对象的销毁不应同时意味着所指向的Device对象的销毁。

这是你作为程序设计者才能告诉自己的事情;从你提供的例子中,我很难判断是否是这种情况。

为了帮助你理清思路,你可以问问自己是否有任何对象除了Settings以外,有权持有指向它的Device对象的指针,使其保持存活,而不仅仅是被动观察者。如果确实是这种情况,那么你需要一个共享所有权策略,这就是std::shared_ptr所提供的内容:

#include <memory>

class Device {
};

class Settings {
    std::shared_ptr<Device> device;
public:
    Settings(std::shared_ptr<Device> const& d) {
        device = d;
    }

    std::shared_ptr<Device> getDevice() {
        return device;
    }
};

int main() {
    std::shared_ptr<Device> device = std::make_shared<Device>();
    Settings settings(device);
    // ...
    std::shared_ptr<Device> myDevice = settings.getDevice();
    // do something with myDevice...
}
注意,weak_ptr 是一种观察指针(observing pointer),而不是拥有指针(owning pointer) - 换句话说,如果指向的对象的所有拥有指针超出范围,则它不会保持指向的对象存活。
相对于常规裸指针,weak_ptr 的优势在于您可以安全地确定它是否为空悬挂指针(dangling pointer)(即它是否指向有效的对象,或者指向的对象原本已被销毁)。这可以通过在 weak_ptr 对象上调用 expired() 成员函数来完成。

4
@LKK: 是的,没错。weak_ptr总是替代裸指针进行观察的一种选择。它在某种程度上更加安全,因为您可以检查它是否悬挂在空中之前对其进行解引用,但也会带来一些开销。如果您可以轻松地保证不会解引用悬挂指针,那么使用观察裸指针应该没有问题。 - Andy Prowl
9
在第一种情况下,让getDevice()返回一个引用可能会更好,是吗?这样调用者就不必检查空指针。 - vobject
6
@chico: 不确定你的意思。auto myDevice = settings.getDevice() 将创建一个名为 myDevice 的类型为 Device 的新实例,并从 getDevice() 返回的引用所引用的实例进行复制构造。如果你想让 myDevice 成为引用,你需要使用 auto& myDevice = settings.getDevice()。所以除非我漏掉了什么,否则我们回到了没有使用 auto 的情况下相同的情况。 - Andy Prowl
3
@Purrformance:因为您不想放弃对象的所有权——将可修改的unique_ptr交给客户端会打开客户端从中移动的可能性,从而获得所有权并留下一个空(唯一)指针。 - Andy Prowl
8
@Purrformance:虽然这样做可以防止客户端移动(除非客户端是一个狂热的科学家,对const_cast情有独钟),但我个人不会这样做。它暴露了实现细节,即所有权是唯一的,并通过unique_ptr实现。我这样看待这个问题:如果你想/需要传递/返回所有权,请传递/返回智能指针(unique_ptr或shared_ptr,取决于所有权的类型)。如果你不想/不需要传递/返回所有权,请使用(正确地const限定的)指针或引用,主要取决于参数是否可以为空。 - Andy Prowl
显示剩余18条评论

2
class Device {
};

class Settings {
    std::shared_ptr<Device> device;
public:
    Settings(const std::shared_ptr<Device>& device) : device(device) {

    }

    const std::shared_ptr<Device>& getDevice() {
        return device;
    }
};

int main()
{
    std::shared_ptr<Device> device(new Device());
    Settings settings(device);
    // ...
    std::shared_ptr<Device> myDevice(settings.getDevice());
    // do something with myDevice...
    return 0;
}

week_ptr仅用于参考循环。依赖图必须是无环有向图。在共享指针中,有两个引用计数:一个用于shared_ptr,另一个用于所有指针(shared_ptrweak_ptr)。当所有shared_ptr被移除时,指针将被删除。当需要从weak_ptr获取指针时,如果存在,则应使用lock来获取指针。


所以如果我正确理解你的回答,智能指针确实可以替代裸指针,但不一定是引用? - michaelk
shared_ptr中实际上有两个引用计数吗?你能解释一下为什么吗?据我所知,weak_ptr不需要被计数,因为它在操作对象时只会创建一个新的shared_ptr(如果底层对象仍然存在)。 - Björn Pollex
@BjörnPollex:我为您创建了一个简短的示例:链接。我还没有实现所有内容,只是复制构造函数和lockboost版本在引用计数上也是线程安全的(delete仅调用一次)。 - Naszta
@Naszta:你的例子表明使用两个引用计数是可能实现的,但是你的回答暗示这是必需的,我不认为是这样。你能否在你的回答中澄清这一点? - Björn Pollex
@BjörnPollex:看,这就是boost实现的方式。在boost中,原子计数器用于线程安全。就是这样。如果你不相信我,可以轻松地在boost实现中(或者在VS2012中)进行检查。 - Naszta
1
@BjörnPollex,为了让weak_ptr::lock()判断对象是否已过期,它必须检查包含第一个引用计数和指向对象的指针的“控制块”,因此在仍有任何weak_ptr对象在使用时,控制块不能被销毁,因此必须跟踪weak_ptr对象的数量,这就是第二个引用计数所做的。当第一个引用计数降至零时,对象被销毁,当第二个引用计数降至零时,控制块被销毁。 - Jonathan Wakely

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接