我试图弄清楚alloca()
在内存级别上的实际工作方式。从linux 手册页:
alloca() 函数在调用者的堆栈帧中分配 size 个字节的空间。当调用 alloca() 的函数返回给它的调用者时,这个临时空间会自动释放。
这是否意味着alloca()
将按字节转发堆栈指针n
?或者新创建的内存到底分配在哪里?
这与可变长度数组不完全相同吗?
我知道实现细节可能留给操作系统和其他东西。但我想知道一般来说这是如何完成的。
是的,alloca
在功能上等同于局部可变长度数组,即:
int arr[n];
还有这个:
int *arr = alloca(n * sizeof(int));
两者都为堆栈上n
的类型元素分配空间。int
每种情况之间的唯一区别arr
是 1) 一个是实际数组,另一个是指向数组第一个元素的指针,以及 2) 数组的生命周期以其封闭范围结束,而alloca
内存的生命周期在函数结束时结束返回。在这两种情况下,数组都驻留在堆栈上。
例如,给定以下代码:
#include <stdio.h>
#include <alloca.h>
void foo(int n)
{
int a[n];
int *b=alloca(n*sizeof(int));
int c[n];
printf("&a=%p, b=%p, &c=%p\n", (void *)a, (void *)b, (void *)c);
}
int main()
{
foo(5);
return 0;
}
当我运行它时,我得到:
&a=0x7ffc03af4370, b=0x7ffc03af4340, &c=0x7ffc03af4320
这表明从返回alloca
的内存位于两个 VLA 的内存之间。
VLA 首次出现在 C99 的 C 标准中,但alloca
在此之前就已经存在了。Linux 手册页指出:
符合
此功能不在 POSIX.1-2001 中。
有证据表明 alloca() 函数出现在 32V、PWB、PWB.2、3BSD 和 4BSD 中。在 4.3BSD 中有一个手册页。Linux 使用 GNU 版本。
BSD 3 可以追溯到 70 年代后期,alloca
在 VLA 被添加到标准之前,早期的非标准化尝试也是如此。
今天,除非您使用不支持 VLA 的编译器(例如 MSVC),否则实际上没有理由使用此功能,因为 VLA 现在是获得相同功能的标准化方法。
另一个答案准确地描述了 VLA 和alloca()
.
alloca()
但是,与自动VLA之间存在显着的功能差异。对象的生命周期。
如果alloca()
函数返回时生命周期结束。对于 VLA,对象在包含块结束时被释放。
char *a;
int n = 10;
{
char A[n];
a = A;
}
// a is no longer valid
{
a = alloca(n);
}
// is still valid
结果,可以轻松地耗尽循环中的堆栈,而使用 VLA 则无法做到这一点。
for (...) {
char *x = alloca(1000);
// x is leaking with each iteration consuming stack
}
对比
for (...) {
int n = 1000;
char x[n];
// x is released
}
尽管从语法的角度来看,alloca 看起来像一个函数,但它不能在现代编程环境中作为普通函数实现*。它必须被视为具有类函数接口的编译器特性。
传统上,C 编译器维护两个指针寄存器,一个“堆栈指针”和一个“帧指针”(或基指针)。堆栈指针界定堆栈的当前范围。帧指针保存了函数入口时堆栈指针的值,用于访问局部变量并在函数退出时恢复堆栈指针。
现在大多数编译器在普通函数中默认不使用帧指针。现代的调试/异常信息格式已经使它变得无用,但他们仍然理解它是什么并且可以在需要的地方使用它。
特别是对于具有 alloca 或可变长度数组的函数,使用帧指针允许函数跟踪其堆栈帧的位置,同时动态修改堆栈指针以适应可变长度数组。
例如,我在 O1 为 arm 构建了以下代码
#include <alloca.h>
int bar(void * baz);
void foo(int a) {
bar(alloca(a));
}
并得到(评论我的)
foo(int):
push {fp, lr} @ save existing link register and frame pointer
add fp, sp, #4 @ establish frame pointer for this function
add r0, r0, #7 @ add 7 to a ...
bic r0, r0, #7 @ ... and clear the bottom 3 bits, thus rounding a up to the next multiple of 8 for stack alignment
sub sp, sp, r0 @ allocate the space on the stack
mov r0, sp @ make r0 point to the newly allocated space
bl bar @ call bar with the allocated space
sub sp, fp, #4 @ restore stack pointer and frame pointer
pop {fp, pc} @ restore frame pointer to value at function entry and return.
是的,alloca 和可变长度数组非常相似(尽管另一个答案指出不完全相同)。alloca 似乎是两个构造函数中较老的一个。
* 使用足够愚蠢/可预测的编译器,可以将 alloca 实现为汇编器中的函数。特别是编译器需要。
这显然是它最初是如何实现的(https://www.tuhs.org/cgi-bin/utree.pl?file=32V/usr/src/libc/sys/alloca.s)。
我想也有可能将实际实现作为汇编函数,但是在编译器中有一个特殊情况,当它看到 alloca 时,它会进入哑/可预测模式,我不知道是否有任何编译器供应商这样做了。
alloca
分配内存,当调用的函数alloca
返回时会自动释放。也就是说,分配的内存alloca
对于特定函数的“堆栈帧”或上下文是本地的。
alloca
不能移植,并且很难在没有传统堆栈的机器上实现。当它的返回值直接传递给另一个函数时,它的使用是有问题的(并且在基于堆栈的机器上的明显实现失败) ,如
fgets(alloca(100), 100, stdin)
如果您在不符合此描述的任何地方使用它,您就是在自找麻烦。alloca()
如果你在这些地方使用,你可能会遇到麻烦,因为此时堆栈上可能有一些东西alloca()
被调用:
if ((pointer_variable = alloca(sizeof(struct something))) == NULL)
{ .... }
而且我希望有人会打电话给我,即使对于某些编译器生成的代码而言,这种高度限制性的限制还不够保守。现在,如果它是作为内置编译器完成的,您可能会设法解决这些问题。
一旦我最终alloca()
弄清楚了这个函数,它就运行得相当好——我记得,它的主要用途是在Bison parser
. 每次调用浪费的 128 个字节加上固定的堆栈大小可能会令人讨厌。为什么我不直接使用GCC
?因为这是尝试将GCC
最初使用交叉编译器的机器移植到一台机器上,结果证明它几乎没有足够的内存来本地编译 GCC(1.35 左右)。GCC 2
出来的时候发现内存够大了,原生编译自己是不可能的。
alloca和VLA之间最重要的区别是失败案例。以下代码:
int f(int n) {
int array[n];
return array == 0;
}
int g(int n) {
int *array = alloca(n);
return array == 0;
}
VLA 不可能检测到分配失败;这是强加于语言结构的非常非 C的事情。因此,Alloca() 设计得更好。