C++ 调用函数时,堆栈帧真的会被推到堆栈上吗?

C++ 调用函数时,堆栈帧真的会被推到堆栈上吗?,c++,assembly,arm,callstack,cpu-registers,C++,Assembly,Arm,Callstack,Cpu Registers,很长一段时间以来,我一直在学习的方法是,当我运行一个程序时,堆栈上立即出现的第一件事是主方法的堆栈帧。如果我在main中调用一个名为foo的函数,那么一个堆栈帧,它是本地变量自动对象和参数的大小,也会被推送到堆栈上 然而,我遇到了一些与此相矛盾的事情。我希望有人能澄清我的困惑,或者解释为什么没有任何矛盾 第一个矛盾: 在Bjarne Stroustrup的C++编程语言第三版中,它在第244页上说,每当在程序执行中遇到声明时,就创建了命名的自动对象。如果这还不够清楚,在下一页,它会说,每次控制线

很长一段时间以来,我一直在学习的方法是,当我运行一个程序时,堆栈上立即出现的第一件事是主方法的堆栈帧。如果我在main中调用一个名为foo的函数,那么一个堆栈帧,它是本地变量自动对象和参数的大小,也会被推送到堆栈上

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

第一个矛盾:

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

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

第二个矛盾:

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

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

<>构建C++对象与获取对象内存几乎无关。事实上,更准确的说法是保留内存,因为一般来说,计算机并没有RAM构建器的小团队,每当您请求一个新对象时,这些团队都会突然行动起来。内存或多或少是永久性的,尽管我们可能会对VM吹毛求疵。当然,编译器必须安排其程序一次只为一件事使用特定范围的内存。这可能也可能确实要求它在对象存在之前保留一定范围的内存,并在对象消失后的一段时间内避免将其用于其他对象。为了提高效率,编译器甚至可以在对象具有动态存储持续时间的情况下,通过同时保留多个内存块来优化保留,如果它知道它将需要这些内存块的话。在任何情况下,当C++谈论构造对象时,它意味着:用一个不确定的内容来存储一系列内存,并做必要的事情来创建对象的表示以及世界状态中的任何其他东西,这是由对象的创建所暗示的,这可能不限于特定的内存块。 不需要存在堆栈帧。不需要存在堆栈。这一切都取决于编译器。当然,大多数编译器都会生成使用堆栈的代码,好的编译器会发现何时可以缩写甚至省略堆栈帧。因此,是的,框架的大小可能会有所不同

<>构建C++对象与获取对象内存几乎无关。事实上,更准确的说法是保留内存,因为一般来说,计算机并没有RAM构建器的小团队,每当您请求一个新对象时,这些团队都会突然行动起来。内存或多或少是永久性的,尽管我们可能会对VM吹毛求疵。当然,编译器必须安排其程序一次只为一件事使用特定范围的内存。这可能也可能确实要求它在对象存在之前保留一定范围的内存,并在对象消失后的一段时间内避免将其用于其他对象。为了提高效率,编译器甚至可以在对象具有动态存储持续时间的情况下,通过同时保留多个内存块来优化保留,如果它知道它将需要这些内存块的话。在任何情况下,当C++谈论构造对象时,它意味着:用一个不确定的内容来存储一系列内存,并做必要的事情来创建对象的表示以及世界状态中的任何其他东西,这是由对象的创建所暗示的,这可能不限于特定的内存块。 不需要存在堆栈帧。不需要存在堆栈。这一切都取决于编译器。当然,大多数编译器都会生成使用堆栈的代码,好的编译器会发现何时可以缩写甚至省略堆栈帧。所以,是的, 框架的大小可能不同


关于你的第一个问题:

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

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

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

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

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


编译器无法猜到这一点,但它可以看到变量何时开始使用或不再需要,并以最小化内存访问的方式安排事情,从而减小堆栈大小。例如,它可以实现这样一种策略,即一些寄存器应由被叫方保存,另一些寄存器应由被叫方保存并分配或其他任何内容。

关于您的第一个问题:

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

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

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

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

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


编译器无法猜到这一点,但它可以看到变量何时开始使用或不再需要,并以最小化内存访问的方式安排事情,从而减小堆栈大小。例如,它可以实现这样一种策略,即一些寄存器应由被调用方保存,另一些寄存器应由被调用方保存并分配或其他任何内容。

您完全正确,不需要堆栈帧。堆栈帧是管理本地空间问题的快速而肮脏的解决方案,在函数执行过程中,调试比管理堆栈指针的更改更容易。如果函数中需要堆栈,只需在条目上调整堆栈指针,并在返回时恢复堆栈指针就更容易了

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

正如你在课堂上所学到的,通过实验很容易看出,编译一些简单的函数,具有不同的优化级别,从没有优化到一些,例如gcc不会使用堆栈,除非它必须使用。我们在这里讨论的是arm,正常的调用约定是基于寄存器的,没有什么规定编译器作者必须遵循
按照这种约定,如果编译器选择使用基于arm的堆栈,则可以使用基于arm的堆栈。常规约定基于堆栈的处理器,因为代码已经在处理堆栈,所以它可以选择使用堆栈帧。在这些情况下,可能会使用基于堆栈的约定,因为处理器缺少通用寄存器,并且比具有更多寄存器的其他处理器更依赖堆栈,这意味着处理器可能不仅需要堆栈用于调用约定,还需要用于大多数本地存储。

您完全正确,不需要堆栈帧。堆栈帧是管理本地空间问题的快速而肮脏的解决方案,在函数执行过程中,调试比管理堆栈指针的更改更容易。如果函数中需要堆栈,只需在条目上调整堆栈指针,并在返回时恢复堆栈指针就更容易了

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


正如你在课堂上所学到的,通过实验很容易看出,编译一些简单的函数,具有不同的优化级别,从没有优化到一些,例如gcc不会使用堆栈,除非它必须使用。我们在这里讨论的是arm,正常的调用约定是基于寄存器的。没有什么规定编译器作者必须遵循该约定,如果编译器选择这样做,那么可以使用基于arm的堆栈。常规约定基于堆栈的处理器,因为代码已经在处理堆栈,所以它可以选择使用堆栈帧。在这些情况下,可能会使用基于堆栈的约定,因为处理器缺少通用寄存器,并且比具有更多寄存器的其他处理器更依赖堆栈,这意味着处理器可能需要堆栈,这不仅仅是为了调用约定,也是为了大多数本地存储。

这些都是非常好的答案。因此,编译器是否要一次分配全部内存取决于它是否要忽略某些变量可能由于if-else之类的条件语句而不需要的事实,或者通过逻辑来查看什么是绝对必要的?@KacyRaye:绝对必要。例如,Gcc将在堆栈框架中为函数中最大范围的自动变量分配足够的内存。例如,void f{{int-tiny[3];g;}{int-巨人[10000];g;};对g的两个调用在条目上具有相同的堆栈指针。也就是说,要有足够的空间容纳小的或大的,但不能两者兼而有之。这些都是非常好的答案。因此,编译器是否要一次分配全部内存取决于它是否要忽略某些变量可能由于if-else之类的条件语句而不需要的事实,或者通过逻辑来查看什么是绝对必要的?@KacyRaye:绝对必要。例如,Gcc将在堆栈框架中为函数中最大范围的自动变量分配足够的内存。例如,void f{{int-tiny[3];g;}{int-巨人[10000];g;};对g的两个调用在条目上具有相同的堆栈指针。也就是说,在有足够的空间容纳小的或大的,但不是两个。你可能想看看,主要是我们希望编译器尽可能快,所以没有关于它们必须如何生成汇编代码的一般规则,特别是在优化的情况下。如果编译器可以推断出一些东西,比如在叶函数中,它将利用这一事实。如果对象不是POD并且不知道构造函数实现,编译器必须为该对象创建空间;如果它想减少堆栈使用、opt cache等,它可能会立即分配,也可能不会。您可能想看看,主要是我们希望编译器尽可能快,所以没有关于它们必须如何生成汇编代码的一般规则,特别是在优化的情况下。如果编译器可以推断出一些东西,比如在叶函数中,它将利用这一事实。如果对象不是POD并且不知道构造函数实现,编译器必须为该对象创建空间;如果想要减少堆栈使用、选择缓存等,它可以立即分配,也可以不分配。