C语言中奇技淫巧08-使用alloca/__builtin_alloca从栈上分配空间
alloca
是什么?
alloca 是一个非标准但广泛支持的 C 语言函数,用于在当前函数的栈(stack)上动态分配内存。
- 与 malloc 的区别:
- malloc 在堆(heap) 上分配内存,需要手动调用 free 释放。
- alloca 在栈上分配内存,函数返回时会自动释放,无需手动 free。
- 优点:分配和释放速度快(栈操作很快),内存自动管理。
- 缺点:
- 分配的内存大小受限于栈空间(通常比堆小得多),分配过大可能导致栈溢出(stack overflow)。
- 不是 C 标准的一部分,可移植性较差。
- 在循环中使用可能导致栈持续增长(虽然函数返回会释放,但循环内反复调用仍可能有问题)。
__builtin_alloca
是什么?
__builtin_
开头的函数是 GCC(GNU Compiler Collection)等编译器提供的“内置函数”(built-in functions)。
- __builtin_alloca 是 GCC 对 alloca 功能的编译器级实现。
- 它不是链接时从库中加载的普通函数,而是在编译阶段由编译器直接生成相应的汇编代码(通常是调整栈指针 esp/rsp)。
- 这使得它比调用一个普通的库函数 alloca 更高效,也更底层。
- 示例
#include <stdio.h>// 假设这个宏已经被定义(在某些系统头文件中常见)
// #define alloca(size) __builtin_alloca(size)void example(int n) {// 在栈上分配 n 个 int 的空间int *arr = (int*)alloca(n * sizeof(int));// 使用分配的内存for (int i = 0; i < n; i++) {arr[i] = i * i;}printf("arr[5] = %d\n", arr[5]); // 假设 n > 5// 函数返回时,arr 所指向的栈内存自动释放// 无需 free(arr)
}int main() {example(10);return 0;
}
在这个例子中,alloca(n * sizeof(int)) 会被预处理器替换为 __builtin_alloca(n * sizeof(int)),然后编译器直接生成调整栈指针的指令来完成内存分配。
- 重要注意事项
- 不要在非叶函数(non-leaf function)中使用 alloca:如果函数 A 调用了 alloca,然后又调用了其他函数 B,B 的栈帧可能会覆盖 A 中 alloca 分配的区域,导致未定义行为。
- 避免在循环中使用:虽然安全,但可能导致栈空间持续增长。
- 检查返回值:alloca 和 __builtin_alloca 在分配失败(如栈溢出)时不会返回 NULL,而是导致程序崩溃。因此无法像 malloc 那样检查错误。
- 可移植性问题:尽管广泛支持,但在某些编译器或系统上可能不可用。更现代、更安全的替代方案是使用 变长数组(VLA, Variable Length Array)(C99 标准支持,但 C11 不强制要求),例如:
void func(int n) {int arr[n]; // C99 VLA,在栈上分配// ...
} // arr 自动释放
- Linux ManualPage中是这样描述的
//在栈上分配内存,并在函数返回时自动释放
#include <alloca.h>void *alloca(size_t size);
- 分配位置: 在调用者的栈帧(stack frame)中分配内存。
这意味着 alloca 分配的内存属于调用它的那个函数的栈空间。
- 自动释放: 当调用 alloca 的函数返回时,这块内存会自动被回收(通过栈指针的移动)。
- 返回值:
- 成功时:返回指向分配内存的指针。
- 失败时(栈溢出):未定义行为(undefined behavior)。
这是 alloca 最大的风险之一。它不会返回 NULL 来指示失败。如果分配的内存过大,导致栈溢出,程序很可能直接崩溃(如段错误),且无法在代码中安全地检测和处理这种错误。
- 属性:
- MT-Safe 表示该函数是线程安全的(Multi-Thread Safe)。
- 原因:每个线程都有自己的栈,alloca 在当前线程的栈上分配内存,不会与其他线程的栈发生冲突。因此,多个线程同时调用 alloca 是安全的。
- STANDARDS (标准)
None.
- 这是最关键的一点:alloca 不属于任何正式的 C 语言标准(如 ISO C90, C99, C11, C17)。
- 它是一个非标准扩展,其存在和行为依赖于具体的编译器和操作系统实现。
- 这意味着使用 alloca 的代码可移植性较差,在某些编译器或平台上可能不可用。
- NOTES (注意事项)
- alloca() 函数的实现依赖于具体的机器架构和编译器。因为它是在栈上进行分配,所以它比 malloc(3) 和 free(3) 更快。在某些情况下,对于使用 longjmp(3) 或 siglongjmp(3) 的应用程序,它也可以简化内存释放。除此之外,不鼓励使用 alloca()。
- 由于 alloca() 分配的空间位于栈帧内,如果通过调用 longjmp(3) 或 siglongjmp(3) 跳过了函数的返回过程,那么这部分空间也会被自动释放。
- 如果指向 alloca() 分配空间的指针只是简单地超出了其作用域(例如,在嵌套代码块中定义的指针),那么该空间不会被自动释放。
- 切勿尝试使用 free(3) 来释放 alloca() 分配的空间!
- 由于实现上的需要,alloca() 本质上是一个编译器内置函数(built-in),也被称为 __builtin_alloca()。现代编译器默认会自动将所有对 alloca() 的调用转换为该内置函数。但是,如果请求了标准符合性(如使用 -ansi 或 -std=c* 编译选项),这种自动转换是被禁止的,此时必须包含 <alloca.h> 头文件,否则会产生一个符号依赖。
- alloca() 是一个内置函数这一事实意味着:无法获取它的地址,也无法通过链接不同的库来改变它的行为。
- 变长数组(Variable Length Arrays, VLAs) 是 C99 标准的一部分,自 C11 标准起变为可选特性,可以用于类似的目的。然而,它们无法移植到标准 C++ 中,并且作为变量,它们存在于其代码块的作用域内,没有类似内存分配器的接口,因此不适合用于实现 strdupa(3) 这样的功能。
- BUGS (缺陷)
- 由于栈的特性,无法检查分配操作是否会超出栈的可用空间,因此也无法指示错误。(不过,如果程序试图访问不可用的空间,很可能会收到一个 SIGSEGV 信号。)
- 在许多系统上,不能在函数调用的参数列表中使用 alloca(),因为 alloca() 在栈上保留的空间会出现在函数参数所用空间的中间位置。