【问题标题】:(C++) What happened to an array allocated on the stack when the function is finished?(C++) 当函数完成时,分配在堆栈上的数组发生了什么?
【发布时间】:2013-10-07 22:41:30
【问题描述】:

我从事 Java 开发多年,现在想切换到 C++,我很难理解内存管理系统。

让我用一个小例子来解释一下情况:

据我了解,您可以在堆栈或堆上分配空间。第一个是通过像这样声明一个变量来完成的:

 int a[5]

int size = 10;
int a[size]

相反,如果你想在堆上分配内存,那么你可以使用“new”命令来完成。比如像:

int *a = new int[10]; (notice that I haven't tried all the code, so the syntax might be wrong)

两者之间的一个区别是,如果它在函数完成时分配到堆栈上,那么空间会自动释放,而在另一种情况下,我们必须使用 delete() 显式释放它。

现在,假设我有这样的课程:

class A {
  const int *elements[10];

  public void method(const int** elements) {
    int subarray[10];
    //do something
    elements[0] = subarray;
  }
}

现在,我有几个问题:

  1. 在这种情况下,子数组分配在堆栈上。为什么函数方法完成后,如果我查看 elements[0] 仍然可以看到子数组的数据?编译器是否翻译了堆分配中的第一个分配(在这种情况下,这是一个好的做法)?
  2. 如果我将子数组声明为“const”,那么编译器不会让我将它分配给元素。为什么不?我认为 const 只涉及无法更改指针,而没有其他问题。
  3. (这可能很愚蠢)假设我想分配的“元素”不是固定的 10 个元素,而是来自构造函数的参数。是否仍然可以在堆栈中分配,还是构造函数总是在堆中分配?

很抱歉这些问题(对于专业的 C 程序员来说可能看起来很傻),但是 C++ 的内存管理系统与 Java 非常不同,我想避免泄漏或慢代码。非常感谢!

【问题讨论】:

  • 请注意,您不必明确使用new,有些人甚至认为这是不好的做法。智能指针结合make_shared(或make_unique)提供更多的便利和安全;对于对象数组,有 std::vectorstd::array
  • int size = 10; 必须是const int size = 10;constexpr int size = 10; 并且public 后面缺少一个冒号。

标签: c++ memory-management


【解决方案1】:

a) 在这种情况下,子数组分配在堆栈上。为什么函数方法完成后,如果我查看 elements[0] 仍然可以看到子数组的数据?编译器是否翻译了堆分配中的第一个分配(在这种情况下,这是一个好的做法)?

这被称为“未定义的行为”,任何事情都可能发生。在这种情况下,subarray 保存的值仍然存在,顺便说一句,可能是因为您在函数返回后立即访问了该内存。但是您的编译器也可以在返回之前将这些值清零。您的编译器还可以将喷火龙送到您的家中。任何事情都可能发生在“未定义的行为”中。

b) 如果我将子数组声明为“const”,那么编译器不会让我将它分配给元素。为什么不?我认为 const 只涉及无法更改指针,而没有其他问题。

这是该语言的一个相当不幸的怪癖。考虑

const int * p1; // 1
int const * p2; // 2
int * const p3; // 3
int * p4;       // 4
int const * const p5; // 5

这是所有有效的 C++ 语法。 1 表示我们有一个指向 const intmutable 指针。 2 表示与 1 相同(这是怪癖)。 3 表示我们有一个 const 指针 指向一个 mutable int。 4 说我们有一个普通的旧 可变指针 指向一个 mutable int。 5 表示我们有一个指向 const intconst 指针。规则大致是这样的:从从右到左读取 const,除了最后一个 const,它可以在右边或左边。

c) 假设我想分配的“元素”不是固定的 10 个元素,而是来自构造函数的参数。是否仍然可以在堆栈中分配,还是构造函数总是在堆中分配?

如果您需要动态分配,那么它通常在堆上,但堆栈和堆的概念取决于实现(即无论您的编译器供应商做什么)。

最后,如果您有 Java 背景,那么您需要考虑内存的所有权。例如,在您的方法void A::method(const int**) 中,您将指针指向本地创建的内存,而该内存在方法返回后消失。您的指针现在指向没有人拥有的内存。最好将该内存实际复制到一个新区域(例如,A 类的数据成员),然后让您的指针指向 块内存。 此外,虽然 C++ 可以使用指针,但明智的做法是不惜一切代价避免使用它们。例如,在可能和适当的情况下尽量使用引用而不是指针,并将std::vector 类用于任意大小的数组。这个类也会处理所有权问题,因为将一个向量分配给另一个向量实际上会将所有元素从一个向量复制到另一个向量(现在使用右值引用除外,但暂时忘记这一点)。有些人认为“赤裸裸的”新建/删除是不好的编程习惯。

【讨论】:

  • "如果你需要动态分配,那么它总是在堆上。栈就是这样工作的;"可以在堆栈上分配动态内存量,请参阅 C 的 VLA 和 C++14/TR std::dynarray 以及运行时绑定的数组。
  • stackoverflow.com/questions/19111028/… 我猜你是对的......但是在运行时堆栈的增量仍然是固定的,因为 std::dynarray 的大小在构造后无法更改。
  • 您可以定义两个std::dynarrays。第二个对 BP 和 SP 都没有固定的偏移量。
【解决方案2】:

A) 不,编译器没有翻译它,你也没有冒险进入未定义的行为。要尝试找到与 Java 开发人员相似的地方,请考虑您的函数参数。当你这样做时:

int a = 4;
obj.foo(a);

a 被传递给方法foo 时会发生什么?制作了一个副本,将其添加到堆栈帧中,然后当函数返回时,该帧现在用于其他目的。您可以将局部堆栈变量视为参数的延续,因为它们通常被类似地对待,除非调用约定。我认为阅读有关堆栈(与语言无关的堆栈)如何工作的更多信息可以进一步阐明这个问题。

B)你可以标记指针const,也可以标记它指向的东西const

int b = 3
const int * const ptr = &b;
^            ^
|            |- this const marks the ptr itself const
| - this const marks the stuff ptr points to const

C) 在某些 C++ 标准中可以将其分配到堆栈上,但在其他标准中则不行。

【讨论】:

  • 也许这只是因为我自己还在学习C++,但是你没有把consts 的含义弄混吗?我参考this question(和this comment)获取信息。
  • @ajp15243 你是对的。更喜欢在类型清除 IMO 之后放置 constint const* const ptr。这里,第一个 const 指的是int,而第二个指的是指针。
  • @ajp15243:正确。第一个const 将指向内存的内容标记为const,而第二个使指针值本身const。我建议这样写 ptrs:<type> const * const <name> = <value>;int const * const ptr = &b; 每个 const 适用于左侧。第一个给int(指向值)第二个给*(指针)。
  • @Pixelchemist 该建议也使我链接的问题的答案变得明智:“向后阅读”。
  • yan,你对 C) 的陈述是什么意思?运行时边界数组只允许在 C++1y / 一些 TR 中使用。
【解决方案3】:

Java 和 C/C++ 之间的主要区别之一是显式未定义行为 (UB)。 UB 的存在是 C/C++ 性能的主要来源。 UB 和“不允许”之间的区别在于 UB 是未经检查的,所以任何事情都可能发生。在实践中,当 C/C++ 编译器编译触发 UB 的代码时,编译器将执行任何生成最高性能代码的操作。

大多数时候这意味着“没有代码”,因为你不能比这更快,但有时会有更积极的优化来自 UB 的结论,例如被取消引用的指针不能为 NULL(因为将是 UB),因此稍后对 NULL 的检查应始终为 false,因此编译器将正确地决定可以不进行检查。

由于编译器通常也很难识别 UB(标准并不要求),因此“任何事情都可能发生”确实是正确的。

1) 根据标准,在您离开范围后取消引用指向自动变量的指针是 UB。为什么这行得通?因为数据仍然存在于您离开它的位置。直到下一个函数调用覆盖它。把它想象成你卖掉了一辆汽车。

2) 指针中实际上有两个可能的常量:

int * a;                        // Non const pointer to non const data
int const * b;                  // Non const pointer to const data
int * const c = &someint;       // Const pointer to non const data
int const * const d = &someint; // Const pointer to const data

* 之前的const 指的是数据,* 之后的const 指的是指针本身。

3) 这不是一个愚蠢的问题。在 C 中,在堆栈上分配具有动态大小的数组是合法的,但在 C++ 中则不是。这是因为在 C 中不需要调用构造函数和析构函数。这是 C++ 中的一个难题,并针对最新的 C++11 标准进行了讨论,但决定保持原样:它不是标准的一部分。

那么为什么它有时会起作用?好吧,它适用于 GCC。这是 GCC 的非标准编译器扩展。我怀疑他们只是对 C 和 C++ 使用相同的代码,然后他们“把它留在了那里”。您可以使用 GCC 开关将其关闭,使其以标准方式运行。

【讨论】:

  • @DyP 请注意我是如何写“触发 UB 是性能的主要来源”。此外,UB 并不意味着“不在标准范围内”。这意味着该标准明确指出某物是 UB。 Java 语言规范中没有这样的东西。我读了。
  • 好吧,让我们清理一下 cmets。我仍然不确定“在实践中,当 C/C++ 编译器编译触发 UB 的代码时,编译器将执行产生最高性能代码的任何操作”是什么意思。下面的示例建议类似“在实践中,对于可能导致 UB 的代码(例如,取决于运行时值),编译器不会检查但会生成最高性能的代码。”
  • @DyP 请不要让我打开挑剔者的角落。不要忘记这个答案的目标受众是谁。如果您有兴趣阅读有关 UB 的更多信息,我建议您从这里开始阅读三部曲:blog.regehr.org/archives/213
  • 对不起,我的 -pedantic 开关很久以前在其 ON 位置断开了;)我已经赞成你的答案,但我正在努力成为在我自己的答案中尽可能准确和清晰,因此即使是像这样的非常小的问题也很感激。
【解决方案4】:

a) 你看到它是因为它的堆栈空间还没有被回收。随着堆栈的增长和缩小,此内存可能会被覆盖。不要这样做,结果是不确定的!

b) 子数组是一个整数数组,而不是一个指针。如果是 const,则不能赋值。

c) 根本不是一个愚蠢的问题。您可以使用新的展示位置来做到这一点。也可以使用变量来对堆栈上的数组进行标注。

【讨论】:

    【解决方案5】:

    re a):当函数返回时,数据仍然在你放它的地方,在栈上。但是在那里访问它是未定义的行为,并且该存储将几乎立即被重用。它肯定会在下一次调用任何函数时被重用。这是使用堆栈的方式所固有的。

    【讨论】:

      【解决方案6】:

      该标准没有讨论堆栈或堆,在这种情况下,您的阵列具有自动存储,在大多数现代系统中将在堆栈上。在您退出范围然后访问它后,保留指向自动对象的指针只是简单的undefined behavior3.7.3 部分中的 C++ 标准草案 第 1 段 说(强调我的):

      显式声明的块范围变量 register 或未显式声明的 static 或 extern 具有自动存储持续时间。 这些实体的存储将持续到创建它们的块退出。

      【讨论】:

      • @DyP 正确,添加了更好的措辞。
      猜你喜欢
      • 1970-01-01
      • 2013-06-22
      • 2023-02-22
      • 2015-11-23
      • 2012-08-13
      • 2020-08-22
      • 2019-10-06
      • 2020-04-19
      • 2021-10-11
      相关资源
      最近更新 更多