智能指针:谁拥有对象?

122

C++ 的重点在于内存所有权 - 即所有权语义

动态分配内存的块的所有者有责任释放该内存。因此,问题实际上变成了谁拥有该内存。

在 C++ 中,指针类型所包含的原始指针的所有权是有文档记录的,因此在一个好的(我个人认为)C++ 程序中很少看到传递原始指针的情况(因为原始指针没有隐含的所有权,因此我们无法知道谁拥有该内存,因此如果不仔细阅读文档,则不能确定谁负责所有权)。

相反,在类中很少看到存储原始指针,每个原始指针都存储在其自己的智能指针包装器中。(注意: 如果你不拥有一个对象,就不应该存储它,因为你无法知道它何时将超出作用域并被销毁。)

因此,问题是:

  • 人们遇到了哪种所有权语义?
  • 使用哪些标准类来实现这些语义?
  • 在什么情况下发现它们有用?

让我们对每个答案保留一种语义所有权,以便可以单独进行投票。

总结:

从概念上讲,智能指针很简单,一个天真的实现很容易。我看到过许多尝试实现的例子,但不可避免地会以某种对偶然使用和示例不明显的方式中断。因此,我建议始终使用经过良好测试的库中的智能指针,而不是自己编写。 std::auto_ptr 或 Boost 智能指针之一似乎涵盖了我的所有需求。

std::auto_ptr<T>:

单个人拥有该对象。允许转移所有权。

用途:这允许您定义显示所有权转移的接口。

boost::scoped_ptr<T>

单个人拥有该对象。不允许转移所有权。

用途:用于显示所有权。对象将通过析构函数或显式重置销毁。

boost::shared_ptr<T> (std::tr1::shared_ptr<T>)

多重所有权。这是一个简单的引用计数指针。当引用计数为零时,对象将被销毁。

用法: 当一个对象可以有多个拥有者且其生命周期无法在编译时确定时使用。

boost::weak_ptr<T>:

shared_ptr<T>一起使用,用于处理指针循环引用的情况。

用法: 用于防止循环引用导致对象仅由循环维持共享引用计数。


17
这句话的意思是:“问题是什么?” - Pacerier
12
我只是想指出,自从这个问题被发布后,auto_ptr已经被弃用,现在更推荐使用(已经成为标准的)unique_ptr。 - Juan Campa
在C++中,所有权由原始指针所包装的类型记录,因此在一个好的(我个人认为)情况下,这可以被重新表述吗?我完全不理解它。 - lolololol ol
@lololololol 你把句子切成两半了。在C++中,指针的所有权由其包装的原始指针类型记录,因此在一个好的C++程序中,很少看到原始指针被传递。原始指针没有所有权语义。如果你不知道所有者是谁,你就不知道谁负责删除对象。有几个标准类用于包装指针(如std::shared_ptr、std::unique_ptr等),它们定义了所有权,从而定义了谁负责删除指针。 - Martin York
2
在C++11+中,不要使用auto_ptr!请改用unique_ptr! - val is still with Monica
11个回答

25

简单的 C++ 模型

在我看到的大多数模块中,默认情况下,假定接收指针并不意味着接收所有权。事实上,放弃指针所有权的函数/方法非常少,并且在其文档中明确表达这一事实。

该模型假设用户只拥有显式分配的内容的所有权。其他所有内容都会自动处理(在范围退出时或通过 RAII)。这是一个类似于 C 的模型,扩展了大多数指针由将自动或在需要时(主要在对象销毁时)释放它们的对象拥有(RAII 再次帮助你),并且对象的生命周期是可预测的。

在该模型中,原始指针自由流通,大多数情况下不会很危险(但如果开发人员足够聪明,就会尽可能使用引用)。

  • 原始指针
  • std::auto_ptr
  • boost::scoped_ptr

智能指针的 C++ 模型

在充满智能指针的代码中,用户可以忽略对象的生命周期。所有者从不是用户代码:而是智能指针本身(再次是 RAII)。问题在于,循环引用与引用计数智能指针混合使用可能会导致灾难性后果,因此您必须同时处理 shared_ptr 和 weak_ptr。因此,仍然需要考虑所有权(尽管 weak_ptr 可能指向空值,但其优点是可以告诉您)。

  • boost::shared_ptr
  • boost::weak_ptr

结论

除非例外,否则接收指针并不意味着接收所有权,即使对于大量使用引用和/或智能指针的 C++ 代码,知道谁拥有谁仍然非常重要


21

对我来说,这3种类型涵盖了我大部分的需求:

shared_ptr - 引用计数,当计数器为零时释放内存

weak_ptr - 与上面相同,但是它是一个“从属于”shared_ptr的‘从属’指针,无法释放内存

auto_ptr - 当创建和释放在同一函数中发生,或者对象必须始终被视为仅有一个拥有者时。当你将一个指针赋值给另一个指针时,第二个指针会从第一个指针中‘偷取’该对象。

我自己实现了这些指针,但它们也可以在Boost中使用。

在这种情况下,我仍然通过引用传递对象(尽可能使用const),被调用的方法必须在调用期间假定对象仍然存在。

还有另一种我使用的指针,我称之为hub_ptr。当您拥有一个对象必须从嵌套在其中的对象中访问时(通常作为虚基类),可以通过向其传递weak_ptr来解决此问题,但它本身没有shared_ptr。因为它知道这些对象不会比它活得更长,所以它向它们传递一个hub_ptr(这只是一个常规指针的模板包装器)。


3
为什么不直接将*this传递给这些对象并让它们将其存储为引用,而是要创建自己的指针类(hub_ptr)?既然你已经承认这些对象将与拥有类同时被销毁,我不明白为什么要绕了这么多弯路。 - Michel
5
这基本上是一个设计合同,旨在明确事情。当子对象接收到hub_ptr时,它知道指向的对象在子对象生命周期内不会被销毁,并且对其没有所有权。容器和所包含的对象都同意遵守一组清晰的规则。如果使用裸指针,这些规则可以记录,但编译器和代码不会强制执行。 - Fabio Ceconello
1
还要注意,您可以使用#ifdef在发布版本中将hub_ptr typedef为裸指针,因此开销仅存在于调试构建中。 - Fabio Ceconello
3
请注意,Boost文档与您对scoped_ptr的描述不一致。它指出scoped_ptr是“不可复制”的,并且所有权不能转移。 - Alec Thomas
3
@Alec Thomas,你说得对。我在想auto_ptr时写成了scoped_ptr。已经更正。 - Fabio Ceconello

10

不要拥有共享所有权。如果你有,确保只与你不能控制的代码共享。

这解决了100%的问题,因为它强迫你理解所有东西是如何相互作用的。


2
从boost库中,还有pointer container库。如果你只会在容器的上下文中使用这些对象,那么这些库比普通的智能指针容器更高效、更易于使用。
在Windows系统中,有COM指针(IUnknown、IDispatch等)以及用于处理它们的各种智能指针(例如ATL的CComPtr和Visual Studio中基于_com_ptr类自动生成的智能指针)。

2
  • 共享所有权
  • boost::shared_ptr

当一个资源被多个对象共享时,使用boost shared_ptr进行引用计数,确保在所有人完成后释放该资源。


2

std::tr1::shared_ptr<Blah>通常是您最好的选择。


2
shared_ptr是最常见的,但还有许多其他类型。每种类型都有自己的使用模式和适合和不适合使用的场景。更详细的描述会更好。 - Martin York
如果你被困在旧编译器中,boost::shared_ptr<blah> 是 std::tr1::shared_ptr<blah> 基于的内容。这是一个足够简单的类,你可以从 Boost 中提取它,即使你的编译器不支持最新版本的 Boost 也可以使用。 - Branan

1

yasper::ptr是一个轻量级的、类似于boost::shared_ptr的替代品。它在我的(当前)小型项目中表现良好。

http://yasper.sourceforge.net/网页中,它被描述如下:

Why write another C++ smart pointer? There already exist several high quality smart pointer implementations for C++, most prominently the Boost pointer pantheon and Loki's SmartPtr. For a good comparison of smart pointer implementations and when their use is appropriate please read Herb Sutter's The New C++: Smart(er) Pointers. In contrast with the expansive features of other libraries, Yasper is a narrowly focused reference counting pointer. It corresponds closely with Boost's shared_ptr and Loki's RefCounted/AllowConversion policies. Yasper allows C++ programmers to forget about memory management without introducing the Boost's large dependencies or having to learn about Loki's complicated policy templates. Philosophy

* small (contained in single header)
* simple (nothing fancy in the code, easy to understand)
* maximum compatibility (drop in replacement for dumb pointers)

The last point can be dangerous, since yasper permits risky (yet useful) actions (such as assignment to raw pointers and manual release) disallowed by other implementations. Be careful, only use those features if you know what you're doing!


1

还有一种常用的单可转移所有权的形式,它比auto_ptr更可取,因为它避免了auto_ptr对赋值语义的疯狂破坏所带来的问题。

我说的是swap。任何具有适当swap函数的类型都可以被构想成某些内容的智能引用,直到所有权被转移到同一类型的另一个实例中,通过交换它们。每个实例保留其身份,但绑定到新内容。这就像一个安全可重新绑定的引用。

(它是智能引用而不是智能指针,因为您不必显式地取消引用它以获取内容。)

这意味着auto_ptr变得不那么必要了 - 它只需要填补类型没有良好的swap函数的空白。但是所有std容器都有。


也许现在它不再那么必要了(我认为scoped_ptr使其变得不那么必要了),但它并没有消失。如果你在堆上分配了一些东西,然后有人在你删除它之前抛出异常,或者你只是忘记了删除它,那么拥有一个交换函数对你来说毫无帮助。 - Michel
这正是我在上一段中所说的。 - Daniel Earwicker

1
  • 一位所有者
  • boost::scoped_ptr

当你需要动态分配内存但想要确保在代码块的每个退出点都能释放它时。

我发现这很有用,因为它可以轻松重新定位和释放,而不必担心泄漏。


1

我认为我从来没有处于共享设计所有权的位置。事实上,从我的角度来看,唯一有效的情况是享元模式。


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