1

很长一段时间以来,我一直被教导的方式是,当我运行程序时,立即进入堆栈的第一件事是 main 方法的堆栈帧。如果我从 main 中调用一个名为 foo() 的函数,那么一个堆栈帧就是局部变量(自动对象)的大小,并且参数也会被推送到堆栈上。

但是,我遇到了一些与此相矛盾的事情。我希望有人能澄清我的困惑或解释为什么真的没有任何矛盾。

第一个矛盾:

在 Bjarne Stroustrup 的《C++ 编程语言》第 3 版一书中,它在第 244 页上说,“每次在程序执行中遇到其声明时,都会创建一个命名的自动对象。” 如果这还不够清楚,在下一页它说,“每次控制线程通过局部变量的声明时,都会执行局部变量的构造函数。”

这是否意味着堆栈帧的总内存不是一次全部分配,而是在遇到变量声明时逐块分配?此外,这是否意味着如果由于 if 语句未遇到变量声明,则每次堆栈帧的大小可能不同?

第二个矛盾:

我在汇编中做了一些编码(具体来说是 ARM),我的课程被教授的方式是,当一个函数被调用时,我们立即使用寄存器并且从未将当前函数的任何局部变量推送到堆栈,除非算法无法使用有限数量的寄存器执行。即便如此,我们也只推送了剩余的变量。

这是否意味着在调用函数时,可能根本不会创建堆栈帧?这是否也意味着堆栈帧的大小可能由于寄存器的使用而不同?

4

3 回答 3

3

关于你的第一个问题:

对象的创建与数据本身的分配无关。更具体地说:对象在堆栈上具有保留空间这一事实并不意味着何时调用其构造函数。

这是否意味着堆栈帧的总内存不是一次性分配的,而是在遇到变量声明时逐块分配?

这个问题实际上是特定于编译器的。堆栈指针只是一个指针,二进制文件如何使用它取决于编译器。实际上有的编译器可能会保留整个激活记录,有的可能只是一点一点地保留,有的可能会根据具体的调用动态地保留它等等。这甚至与优化紧密结合,以便编译器能够以它认为更好的方式安排事情。

这是否意味着在调用函数时,可能根本不会创建堆栈帧?这是否也意味着堆栈帧的大小可能由于寄存器的使用而不同?

同样,这里没有严格的答案。通常编译器依赖于能够以最小化“溢出”(堆栈上)变量的方式分配寄存器的寄存器分配算法。当然,如果您是手工编写汇编代码,您可以决定将特定寄存器分配给整个程序中的特定变量,因为您通过它们的内容知道您希望如何使其工作。

编译器无法猜测这一点,但它可以看到变量何时开始使用或不再需要,并以最小化内存访问(因此堆栈大小)的方式安排事物。例如,它可以实现一个策略,使得一些寄存器应该由被调用者保存,而另一些则由被调用者保存并分配或其他。

于 2013-10-06T02:16:54.083 回答
1
  1. 构造一个 C++ 对象与为该对象获取内存几乎没有关系。事实上,说“保留内存”会更准确,因为一般来说,计算机没有小型 RAM 构建器团队,每当您要求一个新对象时,它们就会立即行动。内存或多或少是永久的(尽管我们可以对 VM 争论不休)。当然,编译器必须安排其程序一次只为一件事使用特定范围的内存。这可能(并且可能确实)要求它在对象存在之前保留一定范围的内存,并避免将其用于其他对象,直到对象消失后的某个时间。为了提高效率,编译器可以(即使在对象具有动态存储持续时间的情况下)通过一次保留几个内存块来优化保留,如果它知道它会需要它们。无论如何,当 C++ 谈论“构造一个对象”时,它的意思只是:获取一个具有未定义内容的内存范围,并做必要的事情来创建对象的表示(以及世界状态中的任何其他内容)对象的创建暗示了这一点,这可能不限于特定的内存块。)

  2. 不需要存在堆栈帧。不需要存在堆栈。这完全取决于编译器。当然,大多数编译器确实会生成使用堆栈的代码,并且好的编译器会弄清楚何时可以缩写甚至省略堆栈帧。所以,是的,框架的大小可能会有所不同。

于 2013-10-06T02:12:09.553 回答
1

你是绝对正确的,不需要堆栈帧。堆栈帧是管理本地空间问题的快速而肮脏的解决方案,比在函数过程中管理堆栈指针的更改更容易调试。如果函数中需要堆栈,则只需在入口处调整堆栈指针并在返回时将其恢复就更容易了。

这也不是非黑即白的,编译器和任何其他程序一样是程序,如果你还不知道,那么你会意识到,给定任意数量的程序员,你将获得同一个问题的多种解决方案。即使程序员的数量是一个人可能会选择一遍又一遍地解决问题,直到他们满意和/或出于任何原因可能会选择发布各种版本。堆栈的使用对于局部变量非常常见,这实际上是您的做法,但这并不意味着您必须使用在入口处创建并在返回时恢复的堆栈框架。

正如您在您的课程中所学到的,并且通过实验很容易看到(编译一些简单的函数,从无优化到一些优化的各种级别),例如 gcc 除非必须,否则不会使用堆栈。我们在这里谈论 arm 的地方,正常的调用约定是基于寄存器的(没有什么说编译器作者必须遵循该约定,如果编译器选择这样做,则可以使用基于 arm 的堆栈)。正常约定是基于堆栈的处理器,因为代码已经在处理堆栈,它可能会选择使用堆栈帧。在这些情况下,可能会使用基于堆栈的约定,因为处理器缺少通用寄存器并且比其他具有更多寄存器的处理器更依赖堆栈,

于 2013-10-06T02:19:30.617 回答