3.2. 什么是内存所有权?

在 C++ 中,内存所有权(memory ownership)指的是,负责管理特定内存分配生命周期的实体。我们很少谈论原始内存的所有权。通常,管理的是驻留在该内存中的对象的生命周期和所有权,而内存所有权实际上只是对象所有权的简称。内存所有权的概念与资源所有权密切相关。首先,内存本身就是一种资源。虽然程序可以管理的资源不仅限于内存,但内存无疑是使用最广泛的资源。其次,C++ 管理资源的方式是让对象拥有这些资源。因此,管理资源的问题就简化为管理拥有这些资源的对象的问题,这正是我们谈论内存所有权时真正指代的含义。在此背景下,内存所有权所涉及的远不止内存本身,管理不当的所有权可能导致程序无法控制的资源发生泄漏、计数错误或丢失 —— 这些资源包括内存、互斥锁、文件、数据库句柄、猫咪视频、航空座位预订,甚至是核弹头。

3.2.1 设计良好的内存所有权

设计良好的内存所有权是什么样的?最初想到的简单答案是,程序的每个点上,都能清楚地知道哪个对象由谁拥有。然而,这过于严格了 —— 程序的大部分并不涉及资源(包括内存)的所有权。这些部分的程序仅是在使用资源。编写此类代码时,只需知道某个特定函数或类不拥有该内存就足够了。至于谁拥有它,完全无关紧要:

struct MyValues { long a, b, c, d; }
void Reset(MyValues* v) {
  // 不关心 v 的所有者是谁,只要我们不造成问题即可
  v->a = v->b = v->c = v->d = 0;
}

在程序的每个点上,要么清楚地知道谁拥有该对象,要么清楚地知道所有者不会改变?这比之前更好,因为大部分代码都属于我们回答中的第二部分,但这过于严格 —— 当获取一个对象的所有权时,通常并不重要知道它是从谁那里获取的:

class A {
public:
  // 构造函数转移所有权,无论原所有者是谁
  A(std::vector<int>&& v) : v(std::move(v)) {}
private:
  std::vector<int> v_; // 现在我们拥有这个对象
};

同样,共享所有权(通过引用计数的 std::shared_ptr 表达)的核心要点就是,不需要知道还有谁拥有该对象:

class A {
public:
  // 不知道谁拥有 v,也不关心
  A(std::shared_ptr<std::vector<int>> v) : v_(v) {}
  // 与任意数量的所有者共享所有权
private:
  std::shared_ptr<std::vector<int>> v_;
};

对设计良好的内存所有权的准确描述需要不止一句引述。

通常,良好的内存所有权具有以下特征:

3.2.2 设计不良的内存所有权

正如良好的内存所有权难以用简单的描述概括,而是通过其满足的一系列标准来界定,不良的内存所有权实践也可以通过其常见的表现形式来识别。通常,良好的设计能清晰地表明某段代码是否拥有某个资源,而糟糕的设计则需要额外的、无法从上下文中推断出来的知识。例如,以下 MakeWidget() 函数返回的对象由谁拥有?

Widget* w = MakeWidget();

当 widget 不再需要时,使用端是否应该将其删除?如果是,应该如何删除?如果决定删除 widget,但使用了错误的方式,例如,对一个实际上并非通过 operator new 分配的 widget 调用 operator delete,必然会导致内存损坏。在最好的情况下,程序都会崩溃:

WidgetFactory WF;
Widget* w = WF.MakeAnother();

Factory 是否拥有它创建的 widgets?当 Factory 对象删除时,会删除这些 widgets 吗?或者,是否应该由使用端来完成这项工作?如果 Factory 知道它创建了什么,并会在适当的时候删除所有这些对象,最终可能会导致内存泄漏(或者更糟,如果这些对象还拥有其他资源):

Widget* w = MakeWidget();
Widget* w1 = Transmogrify(w);

Transmogrify() 是否获取了 widget 的所有权?在 Transmogrify() 处理完之后,widget w 是否仍然存在?如果为了构造一个新的、被变形的 widget w1 而删除了 widget,那现在就有了一个悬空指针。如果 widget 有被删除,但我们却以为它删除了,就会造成内存泄漏。

以免所有不良的内存管理实践,都能通过代码中存在原始指针来识别,这里有一个对原始指针使用问题做出的“膝跳反射式”回应,即一种经常出现的、相当糟糕的内存管理方法:

void Double(std::shared_ptr<std::vector<int>> v) {
  for (auto& x : *v) {
    x *= 2;
  }
};

...
std::shared_ptr<std::vector<int>> v(...);
Double(v);
...

Double() 函数在其接口中声称要共享其参数vector的所有权。然而,这种所有权完全多余 —— Double() 没有理由拥有其参数 —— 并不试图延长该vector的生命周期,也不会将其所有权转移给其他人;只是修改了调用者传入的vector。我们可以合理地预期,该vector由调用者拥有(或者由调用栈中更高层的某个实体拥有),并且当 Double() 将控制权交还给调用者时,该vector仍然存在 —— 毕竟,调用者希望将其元素加倍,大概是为了对这些元素进行其他操作。

尽管这个列表远未穷尽所有情况,但它足以说明,草率对待内存所有权可能导致的一系列问题。在下一节中,我们将回顾 C++ 社区开发出的模式和指南,以帮助避免这些问题,并清晰地表达开发者的意图。