C语言是否需要堆栈和堆才能运行?
C语言需要一种机制来实现自动存储并跟踪函数调用的返回点。实际上,这几乎总是使用堆栈。但是它并不需要堆。
只有在使用类似于malloc
的库函数时才通常需要堆,而不是C语言本身。堆实际上并没有什么神奇的地方 - 你可以用纯C写malloc
和free
。它只是有一个大的static
内存块,并有一个从该块中分配空间的算法。
你问“如果CPU不支持堆栈和堆结构怎么办”?
嗯,堆栈和堆只是由内存和指针构建的。我认为你不会找到任何可以被视为"CPU"的处理器的例子,它没有这个能力。
我不同意:在C标准中甚至没有出现过"stack"这个词。确实需要一些机制来实现自动存储并跟踪函数调用的返回点,但它不一定是CPU堆栈。
这是一个真正有趣的观点。我会修改我的答案。但我怀疑实际上所有的C实现都使用堆栈。你知道有没有不使用堆栈的实现吗?
不,我不知道有任何这样的实现,但是C解释器可以使用一个双向链表的帧。许多处理器没有内置的堆栈:例如,许多RISC架构没有带有隐式堆栈的call
/ret
机制,而是使用可以通过适当生成的代码以各种方式保存的链接寄存器。
IBM System Z使用动态分配来实现自动存储。我知道至少有一个不完全晦涩的系统/C实现是以这种方式工作的,但是我必须通过谷歌来查找是什么系统。这个答案稍微夸大了事实,因为它说"invariably"。
有一些8位微型机没有全宽度的指针寄存器,比如6502和8080 / Z80。如为什么C到Z80编译器生成的代码效果不好?中所讨论的那样,在C实现中支持递归/可重入是可能的,但非常笨拙。在这些系统上的编译器往往古老,并且需要在空间有限的系统上运行,这限制了它们在不需要完整的通用慢速版本时找到窥视孔优化的能力,但是在这些机器上有些事情确实很难。
C语言是否需要堆和栈来运行?
根据C11标准,既不需要堆,也不需要栈来运行。但是在实践中,它们都是有用的。
关于调用栈,它是非常常见的,但是可以"避免"使用:
一些优化编译器可能足够聪明(特别是通过整个程序优化或链接时优化),可以检测到整个程序不需要任何调用栈的情况。许多优化编译器会对一些调用进行内联展开,即使这些函数没有标记为内联。
许多现代处理器(如x86或x86-64、AVR、SPARC、ColdFire即mc68K等)都有硬件调用栈(例如某些"stack pointer"寄存器,用于函数调用)。在那些没有这种硬件栈指针的处理器上(如IBM Z系列大型机、PowerPC、MIPS、RISC-V或者ARM),调用约定(或ABI)可能约定地分配一些寄存器来扮演栈指针的角色。值得一提的是,C甚至可以在理论上为没有任何寄存器或栈指针的随机访问机器实现。
可以想象一种使用延续传递样式来避免调用栈的编译器(有效地在堆或某个堆上"分配"一些"调用帧",可能需要某个垃圾回收器的帮助)。Appel的旧书《Compiling with Continuations》详细解释了这个想法。虽然我无法列出任何使用这种方法的C编译器,但标准允许这种方法。如果你编写了一个从C到SML(或某个Lisp)的编译器,你可以拥有这样的编译器。
关于C堆,malloc函数始终可能失败,这仍然符合标准。我认为这样的malloc是无用的,但可能是最快的。此外,C标准允许嵌入式实现根本没有malloc。
如果你寻找一个几乎是C所需的功能,我建议研究二进制表示。我倾向于认为为十进制计算机(如旧的IBM 1620)或三进制计算机(如Setun)实现C11编译器非常不实际(但原则上是可能的,因为你可以在三进制或十进制计算机上"模拟"一个二进制计算机;它们都是图灵完备的)。然而,在博物馆中你只能找到这些旧的(50年代末,60年代初)计算机,并且它们在C语言发明之前就消失了。
顺便说一句,调用栈和堆在ALGOL和Lisp(以及IPL-V)的实现中早在C之前几十年就已经存在了。
有趣的研究......可惜我只能点赞一次,而且似乎每个人都走了,过周末了(或许在看环法自行车赛的到达)。
C语言本身并不需要堆和栈来运行。首先来看堆,实现一个不提供任何类型的堆的编译器只需在调用`malloc`(或其他内存分配函数)时返回`NULL`即可,这是完全符合标准的行为。
至于栈,在ISO C11标准中没有提到关键字“stack”。编译器实现只需要正确地执行标准中规定的所有操作即可。虽然没有栈会使这个任务变得非常困难,但并非不可能。极端情况下,可以通过递归地内联每个函数调用来实现。这样做会消耗大量的代码和函数特定的数据空间,但是肯定是可行的。然而,这可能会让我考虑迁移到另一种具有栈(和堆)的架构上。
尽管如此,即使架构没有提供堆和栈,也可以通过基本的内存I/O操作来构建它们。事实上,我在十几岁时拥有的早期计算机之一就搭载了RCA 1802 CPU,它没有专用的栈,甚至没有`call`或`ret`指令。但是,它可以使用其标准调用和返回技术(SCRT)很好地处理子程序和栈(根据你的观点,这种方式可能是美丽的或者是丑陋的)。请参考这里的一些更多细节以及其他不寻常的架构。
IBM Z(也称为System z、zSeries等)实际上具有堆(通过从操作系统中分配内存)但没有栈。它通过使用堆内存和某些寄存器(类似于上述链接中提到的RCA芯片)来实现链表栈,这意味着函数的前导部分使用`STORAGE OBTAIN`分配局部函数内存,而尾部部分使用`STORAGE RELEASE`释放内存。
不用说,这会在每个函数的前导和尾部插入大量额外的代码。