什么是栈溢出?
栈溢出 是一种程序运行时错误,指程序使用的栈内存空间超出了系统为其分配的大小,导致数据覆盖了其他重要内存区域(通常是函数调用栈或相邻的栈空间)。

你可以把栈想象成一个后进先出 的盘子架:
- 你放一个盘子上去,这是
函数调用。 - 你再放一个盘子上去,这是
嵌套调用。 - 你必须从最上面的盘子开始取,这是
函数返回。
如果盘子架(栈空间)的高度是有限的,而你不停地往上放盘子,总有一天盘子会掉下来,砸坏架子旁边的其他东西(内存数据),这就是栈溢出。
栈是什么?它用来做什么?
在 C 程序中,内存通常被划分为几个区域,其中与栈溢出最相关的是栈 和堆。
| 特性 | 栈 | 堆 |
|---|---|---|
| 用途 | 存储函数内的局部变量、函数参数、返回地址等。 | 动态内存分配,如 malloc, calloc, new (C++) 分配的内存。 |
| 管理方式 | 自动管理,函数调用时分配,函数返回时自动释放。 | 手动管理,需要程序员显式分配 (malloc) 和释放 (free)。 |
| 大小 | 较小且固定,编译时或程序启动时确定,通常为几 MB。 | 非常大且灵活,受限于系统的可用虚拟内存。 |
| 速度 | 快,入栈和出栈是简单的指针移动操作。 | 慢,需要寻找足够大的连续内存块。 |
| 生长方向 | 从高地址向低地址生长(在大多数现代系统上)。 | 从低地址向高地址生长。 |
栈溢出只发生在栈上,因为它的大小是受限的,堆理论上也会耗尽,但那是“堆溢出”或“内存不足”,情况不同。

栈溢出的常见原因
以下是导致 C 语言程序栈溢出的几个最常见的原因:
无限递归
这是最经典、最直接的原因,函数在没有任何终止条件的情况下不断地调用自己,每次调用都会在栈上创建一个新的栈帧,直到栈空间被耗尽。
示例代码:
#include <stdio.h>
void recursive_function() {
int local_variable; // 每次调用都会在栈上分配空间
printf("Hello from recursion...\n");
recursive_function(); // 无限递归,没有 base case
}
int main() {
recursive_function();
return 0;
}
分析:
main 调用 recursive_function,后者又调用自己,形成一个永不停止的调用链,每次调用,local_variable 都需要新的栈空间,最终导致栈溢出。

递归过深
即使递归函数有正确的终止条件,但如果递归的深度太大(处理一个非常大的数据结构),也可能耗尽栈空间。
示例代码:
#include <stdio.h>
long factorial(int n) {
if (n == 0 || n == 1) {
return 1;
}
return n * factorial(n - 1); // 递归调用
}
int main() {
// 计算 100000 的阶乘,递归深度为 100000
long result = factorial(100000);
printf("Factorial: %ld\n", result);
return 0;
}
分析:
计算 factorial(100000) 需要 100000 次函数调用,每次调用都会在栈上占用空间,对于大多数系统来说,这个深度远远超出了栈的容量,从而导致溢出。
过大的局部变量
在函数内部定义了体积巨大的局部变量(一个巨大的数组),这个数组会被直接分配在栈上,如果它的大小超过了栈的剩余空间,就会导致溢出。
示例代码:
#include <stdio.h>
void function_with_big_array() {
// 尝试在栈上分配一个 10MB 的数组
// 1 * 1024 * 1024 * 10 = 10,485,760 字节
char big_array[10 * 1024 * 1024];
printf("This line may not be reached.\n");
}
int main() {
function_with_big_array();
return 0;
}
分析:
在 function_with_big_array 中,big_array 需要 10MB 的连续栈空间,如果程序的默认栈大小小于 10MB(这在很多嵌入式系统或限制栈大小的环境中很常见),程序就会在进入这个函数时立即崩溃。
如何检测和调试栈溢出?
-
观察崩溃信息:
- 在 Linux/macOS 上,程序崩溃时会打印出
Segmentation fault,使用gdb等调试器附加到进程后,在崩溃时输入where或bt(backtrace) 可以看到函数调用栈,如果栈的深度异常大,或者栈指针 (%rsp/esp) 指向了非法地址,通常就是栈溢出。 - 在 Windows 上,可能会弹出类似
Stack Overflow的错误对话框。
- 在 Linux/macOS 上,程序崩溃时会打印出
-
使用调试器:
- GDB (Linux/macOS): 运行
gdb ./your_program,run,崩溃后用bt查看调用栈。 - LLDB (macOS): 与 GDB 类似。
- Visual Studio (Windows): 在调试器中运行,当程序崩溃时,它会自动中断并显示调用堆栈窗口。
- GDB (Linux/macOS): 运行
-
增加栈大小(临时解决方案):
- Linux/macOS: 可以使用
ulimit命令临时增加栈大小。# 将栈大小设置为 100MB ulimit -s 102400 # 然后运行你的程序 ./your_program
- Windows (链接器选项): 在 Visual Studio 中,可以修改项目属性 -> 链接器 -> 系统 -> 堆栈保留大小/提交大小。
- GCC 编译器: 可以使用
-Wl,--stack,<size>选项。gcc -o your_program your_program.c -Wl,--stack,16777216 # 设置栈大小为 16MB
- 注意: 这只是一个“绕过”问题的方法,而不是“解决”问题的方法,治标不治本,如果真的有逻辑错误,更大的栈只是让崩溃发生得更晚而已。
- Linux/macOS: 可以使用
如何解决和避免栈溢出?
解决递归问题(最佳方案)
将递归算法改写为迭代(循环),迭代使用循环和栈数据结构(由程序员手动管理)来模拟递归过程,但它使用的是堆内存,空间上没有栈大小的限制。
示例:递归 -> 迭代
// 递归版本 (可能导致栈溢出)
long factorial_recursive(int n) {
if (n == 0 || n == 1) return 1;
return n * factorial_recursive(n - 1);
}
// 迭代版本 (安全)
long factorial_iterative(int n) {
long result = 1;
for (int i = 2; i <= n; ++i) {
result *= i;
}
return result;
}
使用尾递归优化
如果递归调用是函数的最后一步操作(称为尾递归),一些编译器(如 GCC 的优化选项 -O2 或 -O3)可以将其优化为迭代,从而避免增加新的栈帧。
示例:尾递归
// 尾递归版本的阶乘
long factorial_tail_recursive(int n, long accumulator) {
if (n == 0 || n == 1) {
return accumulator;
}
// 递归调用是最后一步,并且传递了新的累积值
return factorial_tail_recursive(n - 1, n * accumulator);
}
// 包装函数,提供更友好的接口
long factorial(int n) {
return factorial_tail_recursive(n, 1);
}
注意: 必须确保编译器开启了优化(如 gcc -O2),否则尾递归仍然会消耗栈空间。
为大对象使用动态内存
对于需要大量内存的局部变量,不要在栈上定义,而是使用 malloc (C) 或 new (C++) 从堆上分配。
示例:栈数组 -> 堆数组
#include <stdio.h>
#include <stdlib.h> // 包含 malloc 和 free
void function_with_big_array_on_heap() {
// 在堆上分配 10MB 的内存
char *big_array = (char *)malloc(10 * 1024 * 1024);
if (big_array == NULL) {
printf("Memory allocation failed!\n");
return;
}
printf("Successfully allocated 10MB on the heap.\n");
// ... 使用 big_array ...
// 记得在使用完毕后释放内存!
free(big_array);
}
int main() {
function_with_big_array_on_heap();
return 0;
}
关键点:谁分配,谁释放!使用 malloc 分配的内存必须用 free 释放,否则会导致内存泄漏。
增加系统栈大小(最后手段)
如果确实需要使用深度递归(某些算法的深度无法避免),并且无法改写为迭代,那么可以增加系统为程序分配的栈大小,如前所述,这是一个系统级的配置,通常不推荐作为首选方案,因为它会影响整个系统或用户会话。
| 问题 | 原因 | 解决方案 |
|---|---|---|
| 无限递归 | 函数没有终止条件,无限调用自己。 | 添加 if 条件作为递归的出口。 |
| 递归过深 | 逻辑正确,但数据规模太大,递归深度超过栈容量。 | 改写为迭代,或使用尾递归优化(并确保编译器开启优化)。 |
| 局部变量过大 | 在函数内定义了巨大的数组或结构体。 | 使用 malloc/new 在堆上分配大内存,并在用完后 free/delete。 |
| 栈空间不足 | 系统默认栈太小。 | 临时增加栈大小(ulimit),或从根本上解决代码问题。 |
理解栈溢出的根本原因,并掌握递归转迭代、堆内存分配等技巧,是编写健壮、可靠的 C 语言程序的关键一环。
