调用堆栈和堆栈有啥区别?
Posted
技术标签:
【中文标题】调用堆栈和堆栈有啥区别?【英文标题】:what's the difference between callstack and stack?调用堆栈和堆栈有什么区别? 【发布时间】:2021-05-10 04:50:19 【问题描述】:我想我可能问了一个非常错误的问题,但我真的试图通过谷歌搜索来理解它,但没有运气。
我们知道,我们有一个栈和堆。动态分配的堆,局部变量的堆栈等等
假设我有以下 c++ 代码。
void bla(int v1, int v2, int v3)
int g = v1 + v2+ v3;
void nice(int g)
int z = 20;
int k = 30;
bla(g, z, k);
int main()
cout<<"Hello World";
nice(40);
现在,让我们假设有一个堆栈。我知道例如值z,k,g
将存储在堆栈中。但是当我调用函数nice
调用bla
时,那些存储在哪里?我读过每个函数执行都会导致调用堆栈大小增加 1。我想说即使创建局部变量也会导致调用堆栈增加 1。
那么,这些(callstack
, stack
) 有什么关系?
这是我的假设:
当我们调用nice
时,会创建全新的stack
。在那里,我们存储z and k
。当nice
调用bla
时,现在为bla
创建另一个stack
,第二个堆栈存储v1,v2,v3,g
。等等。每个函数都需要自己的callstack
,但我们也可以称它为stack
。
【问题讨论】:
这能回答你的问题吗? Explain the concept of a stack frame in a nutshell 这将是实现的依赖。除了std::stack
和std::make_heap
系列之外,C++ 本身没有堆和栈的概念。相反,它具有自动和动态的存储持续时间,并且描述了应该如何销毁这些对象。从理论上讲,创建一个不使用堆栈并在堆中分配所有内存的实现是完全有效的。
@PaulSanders 我看到了这个,但我希望能就我的假设是否正确以及堆栈和调用堆栈之间的实际区别进行更多讨论
一些架构将调用堆栈(返回地址堆栈)和用于寄存器(需要恢复)的单独数据堆栈和用于变量的自动存储分开。
回复:I've read that each function execution causes call stack size to increase by 1
- 这不是真的,或者说是过于简单化了。如果你想了解血淋淋的细节,你应该阅读calling conventions,但请注意,对于日常 C++ 开发,你根本不需要知道这一点。
【参考方案1】:
堆栈表示由一组通常相似的项目组成的数据结构,并具有以下能力:
push:将一个项目添加到堆栈的“顶部”,它将成为新的顶部 pop:从顶部移除item,因此之前在顶部的item将再次成为顶部;此操作返回您删除的项目 top:获取堆栈的顶部项目,而不修改堆栈您的记忆中有一个称为“堆栈”的部分,正如您正确理解的那样,函数存储在其中。那是调用堆栈。但是,堆栈和调用堆栈并不是 100% 等效的,因为堆栈用于许多其他情况,基本上是在您需要 LIFO(后进先出)处理时。例如,它用于 CPU 的 LIFO 策略,或者当您在图形中进行深度优先搜索时。总之,栈是一种数据结构,可以应用在很多情况下,内存中的调用栈就是一个突出的例子。
那么,为什么要使用堆栈将函数调用存储在内存中。让我们考虑嵌入式函数调用,例如:
f1(f2(...(fn(x))...))
根据经验,为了计算 fi, 1
【讨论】:
谢谢@lajos。有一种 Solidity 语言和以太坊虚拟机说明了这一点:External function calls can fail any time because they exceed the maximum call stack of 1024
。我认为,这句话是误导。当您添加新的局部变量时,调用堆栈甚至会增长。我认为 1024 是递归深度的限制,而不是调用堆栈深度。你不同意吗?
@NikaKurashvili 内存中的堆栈部分是存储函数调用的地方。但是,调用堆栈是实际调用的实际堆栈。所以,如果你调用 f1,那么你有一小堆单个项目存储在内存的堆栈部分中。当 f1 调用 f2 时,所讨论的调用堆栈从 1 个函数增加到两个函数。等等。因此,虽然内存中的堆栈部分大小保持不变,但其内容会发生变化,并且您的实际调用堆栈会随着嵌入式函数调用的发生而增加,并随着嵌入式函数的评估而减少。
@NikaKurashvili 因此,引用的语句实际上告诉您可以执行外部函数调用,但是由于调用堆栈的最大限制为 1024,它们可能会失败。这意味着可能使用递归。
我还是不明白。即使在调试器中,我也可以看到调用堆栈和堆栈是完全不同的东西。 collabshot.com/show/cf0702
@NikaKurashvili 考虑一篮苹果。篮子是容器,苹果是内容物。内存中的栈是存储函数的容器,也就是我们类比的篮子,而函数是内容,也就是篮子。当我们谈到调用堆栈大小时,我们实际上想知道篮子里苹果的数量。篮子可能是空的。或者它可能包含一个苹果等。【参考方案2】:
每个正在运行的进程都分配了一块内存,它称为“堆栈”。而且,这块内存区域都用来表示“调用/返回序列”(通过所谓的“堆栈帧”),和所谓的“局部变量”被调用的每个例程使用。它们是一回事。
通常,不同的 CPU 寄存器用于同时指向每个事物。一个寄存器指向“局部变量”值,而一个完全不同的寄存器指向“堆栈帧”。 (在解释器中,使用了类似的机制。)还有第三个寄存器指示当前的“栈顶”。
在开始执行每个新函数时,“栈顶”只是简单地向前移动以考虑局部变量,之后“局部变量指针”会记住它曾经是什么。
当“从子例程返回”发生时,“栈顶指针”会恢复到某个先前的值,而曾经存在于“它之上”的所有内容现在都已被遗忘了。 因为它不再重要。
【讨论】:
所以,堆栈是一个巨大的堆栈,它由许多堆栈帧(与函数调用一样多)组成,并且运行进程不会创建不同的堆栈帧,它只是将所有内容放在同一个堆栈 没错。 “堆栈帧”实际上是至少向后指向彼此的数据结构。 “局部变量”存在于它们之间的空间中。这样,即使“[所谓的'递归']函数调用自身”,每个所谓的“实例”都是独立的。 “局部变量”(相对于“全局变量”)的位置定义相对 到“最顶层堆栈帧”位置,始终在堆栈中“上方”的方向。 "我们开始...让我们'调用一个函数'。" 首先,我们在堆栈顶部创建一个新的“堆栈框架”,然后将其链接到前一个。接下来,我们推进“栈顶指针”以允许局部变量,在首先存储其先前的值之后,以便我们可以使用它通过相对偏移量访问这些变量。跨度> 某些实现的调用堆栈限制为 1024,比如说。当您使用参数调用函数时,堆栈中添加了超过 1 个项目。那么调用栈帧如何增加 1 呢? “如果堆栈限制为 XXX,而你‘炸毁它’,那么你当然会死。”但这不再发生了。这里是这样的:“我们开始......让我们'调用一个函数'。”首先,我们在堆栈顶部创建一个新的“堆栈框架”,然后将它链接到前一个。接下来,我们推进“栈顶指针”以允许局部变量,在首先存储其先前的值之后,以便我们可以使用它通过相对偏移量访问这些变量。当它是是时候返回了,我们只是简单地向后链接到上一个堆栈帧,然后使用它来相应地重置“堆栈顶部”。【参考方案3】:那么,这些(callstack,stack)到底是如何相关的?
它们非常相关。他们是一样的东西。也叫执行栈。
不要将这个“callstack”与“stack”数据结构的一般概念相混淆。调用堆栈称为 a 堆栈,因为它描述了调用堆栈的结构。
导致调用堆栈大小增加 1
肯定是“1”,但它的增加单位是什么?调用函数时,堆栈指针会增加一个堆栈帧。堆栈帧的大小(以字节为单位)各不相同。函数的框架足够大,可以包含所有局部变量(参数也可以存储在堆栈中)。
因此,如果您希望以字节为单位来衡量增量,那么它不是 1,而是大于或等于 0 的某个数字。
我想说即使创建局部变量也会导致调用堆栈增加 1。
正如我所描述的,拥有一个局部变量会影响调用函数时堆栈指针的递增方式。
当我们调用 nice 时,会创建全新的堆栈。
不,同一个堆栈由整个执行线程中的所有函数调用共享。
几乎所有这些都不是由 C++ 语言指定的,而是在典型情况下适用于大多数 C++ 实现的实现细节,但为了更容易理解而进行了简化。
【讨论】:
某些实现的调用堆栈限制为 1024,比如说。当您使用参数调用函数时,堆栈中添加了超过 1 个项目。那么调用栈帧是如何增加 1 的呢? @NikaKurashvili 您可能想到的 1024 的“堆栈限制”是 1024 KB(这是 Windows 上的默认值)。当您调用一个函数时,堆栈指针会增加该函数帧所需的字节数。如果存在局部变量需要 X 字节,则堆栈指针会增加 X 字节。 到 1024,我的意思是调用堆栈深度。这与千字节无关。我要问的是,如果堆栈大小限制设置为 1024,比方说,为什么它让我们调用递归函数 1024 次?该函数还具有局部变量,应尽快达到堆栈大小限制(1024) @NikaKurashvili 我从未遇到过对嵌套函数调用数量有硬性限制的 C++ 实现。堆栈的大小始终以字节为单位。 这是solidity(以太坊智能合约语言)。我在 C++ 中问过,因为我知道更多人会看到这一点,并且认为相同的上下文也适用于 C++。所以在solidity 中,它对我来说仍然没有意义。以上是关于调用堆栈和堆栈有啥区别?的主要内容,如果未能解决你的问题,请参考以下文章