(C++)函数完成后,堆栈上分配的数组发生了什么变化

(C++) What happened to an array allocated on the stack when the function is finished?

本文关键字:分配 数组 发生了 变化 什么 堆栈 C++ 函数      更新时间:2023-10-16

我来自多年的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. 在这种情况下,子阵列在堆栈上分配。为什么函数方法完成后,如果我查看元素[0],我仍然看到子数组的数据?编译器是否翻译了堆分配中的第一个分配(在这种情况下,这是一种好的做法(?
  2. 如果我将子数组声明为"const",那么编译器不允许我将其分配给元素。为什么不呢?我认为 const 只涉及无法更改指针,而没有别的。
  3. (这可能是相当愚蠢的(假设我想分配"元素"不是固定的 10 个元素,而是来自构造函数的参数。是否仍然可以在堆栈中分配它,或者构造函数将始终在堆中分配它?

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

a( 在这种情况下,子数组在堆栈上分配。为什么函数方法完成后,如果我查看元素[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 int 的可变指针。 2 表示与 1 相同(这是怪癖(。 3 表示我们有一个指向可变 int 的 const 指针。 4 表示我们有一个指向可变 int 的普通旧

可变指针。 5 表示我们有一个指向 const intconst 指针规则大致是这样的:从右到左读取 const,除了最后一个常量,它可以在右边或边。

c( 假设我想分配"元素"不是固定的 10 个元素,而是来自构造函数的参数。是否仍然可以在堆栈中分配它,或者构造函数将始终在堆中分配它?

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

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

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++标准中可以将其分配给堆栈,但在其他标准中则不能。

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

大多数时候,这意味着"没有代码",因为你不能得到比这更快的速度,但有时有更积极的优化来自 UB 的结论,例如被取消引用的指针不能为 NULL(因为那将是 UB(,所以稍后对 NULL 的检查应该始终是假的,因此编译器将正确地决定可以保留检查。

由于编译器通常也很难识别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 的非标准编译器扩展。我怀疑他们只是对 C 和 C++ 使用相同的代码,然后他们"把它留在那里"。您可以在GCC开关的情况下将其关闭,使其以标准方式运行。

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

b( 子数组是一个整数数组,而不是指针。 如果它是常量,则无法分配给它。

c( 根本不是一个愚蠢的问题。 您可以使用新版位来做到这一点。 也可以使用变量来标注堆栈上的数组。

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

该标准不谈论堆栈或堆,在这种情况下,您的阵列具有自动存储,在大多数现代系统中,该存储将在堆栈上。退出范围后保留指向自动对象的指针,然后访问它,这只是简单的未定义行为。第3.7.3节第1段中的C++标准草案说(强调我的(:

显式声明

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