为什么我不应该总是使用shared_ptr和unique_ptr,而应该使用普通指针?

5

我有C#和obj-c的背景,所以RC/GC是我非常重视的东西。当我开始深入学习C++时,我不禁在想,既然普通指针是如此不受管理,为什么我要使用它们而不是其他替代方案呢?

shared_ptr提供了一种很好的方式来存储引用,而不会丢失跟踪它们而删除它们。我可以看到普通指针的实际应用,但它们似乎只是不良实践。

有人可以介绍一下这些替代方案吗?


你不能不加思考地随处使用shared_ptr(相比之下,你可以为所有东西使用GC管理的引用),因为这样做会导致你在不可避免地创建循环引用时感到惊讶。当你编写C++程序时,你必须考虑所有权问题。这并不是说你不应该使用它们(你应该使用),但选择正确的选项并不是一件轻而易举的事情。 - user395760
此外,复制shared_ptr并使其超出范围会产生明显的开销,因为它需要原子操作(在最好的情况下)或锁定(在最坏的情况下)(标准保证这是线程安全的)。复制原始指针几乎没有任何操作。 - Damon
@Damon 实际上,在 C++ 中,你可能比在 C# 中更少考虑所有权的问题,因为你应该使用值语义(而且你不必考虑谁拥有堆栈上的对象)。而我在 Java 中遇到了问题,因为所有权(在最广泛的意义上)不清楚。(例如,谁“拥有”由 java.awt.Component.getSize() 返回的 java.awt.Dimension?) - James Kanze
所以非托管指针,是啊,时间飞逝。 - user1095108
5个回答

11

当然,如果它们是拥有指针,那么鼓励使用shared_ptr和unique_ptr。但是,如果你只需要一个观察者,那么裸指针就足够了(指向的对象不承担任何责任的指针)。

对于std::unique_ptr基本上没有额外开销,而std::shared_ptr会有一些开销,因为它为您执行引用计数,但在这里很少需要节省执行时间。

此外,如果可以通过设计保证生命周期/所有权层次结构,比如树中的父节点存活时间超过其子节点,那么就不需要“智能”指针了——尽管这与指针实际上是否拥有某个东西略有关系。


6
问题其实更应该是相反的:为什么要使用智能指针?与C#(和我认为的Obj-C)不同,C++广泛使用值语义,这意味着除非对象具有应用程序特定的生命周期(在这种情况下,没有任何智能指针适用),否则通常会使用值语义和无动态分配。有例外情况,但是如果您始终考虑值语义(例如像int一样),在必要时定义适当的复制构造函数和赋值运算符,并且只有在对象具有外部定义的生命周期时才进行动态分配,您将发现很少需要做其他任何事情;一切都会自己处理。在大多数良好编写的C++中,使用智能指针非常罕见。

你的答案不仅是错误的,而且会误导新开发人员。在处理容器或从配置文件中获取N时,您通常需要使用动态对象。在大多数复杂应用程序(如游戏)中,您可能会看到对象漂浮并成为多个容器的成员。您无法仅使用按值语义来处理此类情况。查看像虚幻引擎这样的大型代码库(在我看来非常优秀的代码),您将随处可见大量动态分配的对象。 - Shital Shah

6
正如其他人所提到的,在C++中您必须考虑所有权。尽管如此,我正在开发的3D网络多人射击游戏有一个官方规定叫做“不使用new或delete”。它只使用共享和独占指针来指定所有权,并使用从它们(使用.get())检索到的原始指针在我们需要与C API交互的所有地方。性能损失是不明显的。我用这个例子来说明由于游戏/模拟通常具有最严格的性能要求,因此可以忽略不计的性能损失。
这也大大减少了调试和查找内存泄漏问题的时间。理论上,一个设计良好的应用程序永远不会遇到这些问题。然而,在现实生活中,面对截止日期、遗留系统或设计不良的现有游戏引擎等大型项目时,除非您使用智能指针,否则它们在某种程度上是不可避免的。如果您必须动态分配并且没有足够的时间进行资源管理的设计/重写或者调试问题,而且想尽快启动项目,那么智能指针是一个好选择,即使在大规模游戏中也没有明显的性能成本。

1
为什么要使用动态分配?如果是因为复制对象太昂贵(在游戏软件中非常可能出现的情况),那么智能指针(可能是某种侵入式智能指针,而不是std::shared_ptr)可能是适合的选择。但在很多情况下,这并不是必须的。例如:智能指针无法正确处理实体对象,当Weapon收到hitByDisintegratorBlast事件时,它可能会执行delete this,这将无法与智能指针一起使用。 - James Kanze
是的,那似乎相当武断 :). - ScarletAmaranth
我很好奇你认为在游戏中如何不使用动态分配对象?纹理、着色器、网格、动画、声音、音乐等都非常大,它们的所有权由多个其他资源共享。 - derpface
@uberwulu 很棒的回答,我很好奇你在用哪个游戏引擎? - Alex

2
有时候你需要与C API进行交互,这种情况下,你需要至少使用原始指针来处理代码的某些部分。

哦,不错,这个我漏掉了!:) 但是我用漂亮的C++包装它们,然后就不再回头了! - ScarletAmaranth

1
在嵌入式系统中,指针经常用于访问硬件芯片的寄存器或特定地址的内存。由于硬件寄存器已经存在,因此无需动态分配它们。如果删除指针或更改其值,则不会出现内存泄漏。同样适用于函数指针。函数不是动态分配的,并且它们具有“固定”的地址。重新分配函数指针或删除一个不会导致内存泄漏。

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