首页 文章

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

提问于
浏览
4

我来自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;
  }
}

现在,我几乎没有问题:

  • 在这种情况下,子堆栈在堆栈上分配 . 为什么在函数方法完成之后,如果我查看元素[0],我仍然会看到子数组的数据?编译器是否在堆分配中翻译了第一个分配(在这种情况下,这是一个好习惯)?

  • 如果我将子数组声明为"const",则编译器不允许我将其分配给元素 . 为什么不?我认为const只涉及无法更改指针,但没有别的 .

  • (这可能是非常愚蠢的)假设我想分配"elements"不是使用固定的10个元素,而是使用来自构造函数的参数 . 是否仍然可以在堆栈中分配它,或者构造函数将始终在堆中分配它?

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

6 回答

  • 2

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

  • 0

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

    它被称为"undefined behavior",任何事情都可能发生 . 在这种情况下, subarray 持有的值仍然存在,顺便说一句,可能是因为您在函数返回后立即访问该内存 . 但是你的编译器也可以在返回之前将这些值清零 . 您的编译器也可以向您的家中发送喷火龙 . 任何事情都可能发生在"undefined behavior"-land .

    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 int的const指针 . 规则大致是这样的:从右到左读取const,除了最后一个const,它可以在右边或左边 .

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

    如果需要动态分配,那么这通常会在堆上,但堆栈和堆的概念是依赖于实现的(即编译器供应商所做的事情) .

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

  • 1

    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标准中则不行 .

  • 0

    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的非标准编译器扩展 . 我怀疑他们只是为C和C使用相同的代码,他们“把它留在那里” . 您可以关闭GCC开关,使其以标准方式运行 .

  • 0

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

    b)子阵列是整数数组,而不是指针 . 如果是const,则无法分配给它 .

    c)根本不是一个愚蠢的问题 . 您可以使用新的展示位置来完成此操作 . 也可以使用变量来标注堆栈上的数组 .

  • 2

    该标准没有谈论堆栈或堆,在这种情况下,您的阵列具有自动存储,在大多数现代系统中,它将在堆栈中 . 一旦退出范围然后访问它,保持指向自动对象的指针就变得简单undefined behavior . 3.7.3 第1节中的C标准草案说(强调我的):

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

相关问题