遞迴是程式設計的強大工具,但其底層如何運作?本文透過 Visual Studio 反組譯碼 分析階乘遞迴的堆疊幀變化,並展示如何用手寫堆疊替代系統遞迴,深入理解「遞迴本質是堆疊操作」。
/* factorial.c */
#include
int factorial(int n);
int main() {
int fact = factorial(4);
printf("%d\n", fact); // 24
return 0;
}
int factorial(int n) {
if (1 == n) return 1;
return n * factorial(n - 1);
}
; main 內部
push 4 ; 參數入堆疊
call _factorial ; 呼叫函式
add esp, 4 ; 清理參數堆疊空間
mov [fact], eax ; 返回值在 eax
_factorial:
push ebp ; 保存舊堆疊幀指標
mov ebp, esp ; 建立新堆疊幀
sub esp, 0C0h ; 分配區域變數空間
... ; 保存暫存器 ebx/esi/edi
lea edi, [ebp-0C0h]
mov ecx, 30h
mov eax, 0CCCCCCCCh
rep stos dword ptr es:[edi] ; 0xCC 填充(Debug 模式偵測未初始化)
; 核心遞迴邏輯
cmp dword ptr [n], 1
jne L_recursive
mov eax, 1 ; 基底案例:返回 1
jmp L_epilogue
L_recursive:
mov eax, [n]
sub eax, 1
push eax ; 參數 n-1 入堆疊
call _factorial ; 遞迴呼叫自己
add esp, 4 ; 清理參數
imul eax, [n] ; eax *= n (累乘)
L_epilogue:
pop edi / pop esi / pop ebx
add esp, 0C0h ; 釋放區域空間
cmp ebp, esp
call __RTC_CheckEsp ; 堆疊平衡檢查
mov esp, ebp
pop ebp
ret
| 呼叫深度 | 堆疊頂端內容(由高到低) | 說明 |
|---|---|---|
| 0 (main) | ... | 返回位址 | 4 (參數) | | main 呼叫前推參數 |
| 1 | ... | 舊 ebp | 返回位址 | 4 | 區域變數 | 3 (參數) | | factorial(4) 序言後,遞迴呼叫前推 3 |
| 2 | ... | 舊 ebp | 返回位址 | 3 | 區域變數 | 2 (參數) | | factorial(3) 同理 |
| 3 | ... | 舊 ebp | 返回位址 | 2 | 區域變數 | 1 (參數) | | factorial(2) |
| 4 | ... | 舊 ebp | 返回位址 | 1 | 區域變數 | | factorial(1) 觸發基底案例,返回 1 |
| 回溯 | 各層尾聲:imul eax, [n] → 恢復堆疊 → ret | 依次計算 2*1=2, 3*2=6, 4*6=24 |
push 參數,被呼叫端 [ebp+8] 存取(x86 cdecl 慣例)eax,遞迴回溯時逐層 imul 累乘ebp 串聯所有幀,ret 彈出返回位址並跳轉既然遞迴本質是「先進後出」的參數堆疊與回溯運算,我們可以用顯式堆疊資料結構替代。
/* stack_factorial.c */
#include
#include
#define MAX_STACK 100
typedef struct {
int data[MAX_STACK];
int top;
} Stack;
void push(Stack *s, int val) { s->data[s->top++] = val; }
int pop(Stack *s) { return s->data[--s->top]; }
int empty(Stack *s) { return s->top == 0; }
int main() {
Stack st = {.top = 0};
int n, tmp = 1, result = 1;
printf("輸入階乘 n: "); scanf("%d", &n);
while (n > 0) push(&st, n--); // 入堆疊:n, n-1, ..., 1
while (!empty(&st)) {
tmp = pop(&st); // 出堆疊:1, 2, ..., n
result *= tmp; // 累乘
}
printf("%d! = %d\n", n, result);
return 0;
}
| 維度 | 系統遞迴 | 手寫堆疊 |
|---|---|---|
| 程式碼簡潔度 | 高(宣告式) | 低(命令式) |
| 記憶體開銷 | 堆疊幀含返回位址、保存暫存器、區域變數 | 僅資料陣列,輕量 |
| 堆疊溢位風險 | 深度遞迴易溢位(預設 1MB) | 可控(堆積記憶體) |
| 效能 | 函式呼叫開銷 | 迴圈較快 |
| 適用場景 | 樹遍歷、分治演算法、深度未知 | 已知深度、尾遞迴可優化、嵌入式受限環境 |
透過反組譯碼觀察遞迴的堆疊幀建立、參數傳遞、回溯運算全過程,能將「遞迴」從魔法變為可視化的機制。手寫堆疊實現進一步證明:遞迴 = 隱式堆疊 + 回溯邏輯。理解這一點,除錯棧溢位、優化效能、設計非遞迴演算法都將更得心應手。