第1章:递归的陷阱——无限递归与段错误

递归这东西,用好了是神器,用不好就是定时炸弹。

我记得刚入行那会儿,写了一个斐波那契数列的递归函数,自我感觉良好。结果一运行,程序直接卡死,然后弹了个"段错误"。当时我一脸懵——代码逻辑明明没错啊?后来才明白,我掉进了递归最经典的陷阱:无限递归

今天咱们就来聊聊这个坑,以及怎么避开它。

1.1 无限递归是怎么发生的?

说白了,递归就是函数调用自己。但如果你不给它一个"停下来"的信号,它就会一直调下去,直到把栈空间撑爆。

看个最简单的例子:

void bad_recursion() {
    printf("我又调自己了\n");
    bad_recursion();  // 没有终止条件!
}

这个函数会一直打印,直到栈溢出。为什么会这样?

每次函数调用,系统都会在栈上分配一块内存,用来存局部变量、返回地址什么的。递归调用一次,就多占一块。无限递归就等于无限占内存——栈空间就那么大,撑爆了就是段错误。

⚠️ 注意:段错误(Segmentation Fault)是Linux/Unix系统下的说法。Windows下可能叫"访问违规"或"栈溢出"。本质都一样——程序访问了不该访问的内存。

1.2 我踩过的一个坑

我曾经写过一个二叉树遍历的函数,大概长这样:

void traverse(Node* root) {
    if (root != NULL) {
        printf("%d ", root->data);
        traverse(root->left);
        traverse(root->right);
    }
}

看着没问题对吧?但问题出在调用方——我传进去的root指针是野指针!结果函数判断root != NULL为真,然后访问root->data,直接段错误。

嗯,这里要注意:递归的终止条件不仅要检查"是否为空",还要确保传入的参数是合法的

1.3 如何避免无限递归?

其实就三条铁律:

  1. 必须有明确的终止条件——递归到什么时候停?
  2. 每次递归都要向终止条件靠近——参数要变化,不能原地踏步
  3. 检查输入合法性——别让非法数据进入递归

拿计算阶乘来说:

int factorial(int n) {
    // 终止条件
    if (n <= 1) {
        return 1;
    }
    // 向终止条件靠近
    return n * factorial(n - 1);
}

这个函数,n每次减1,最终会到1。完美。

但如果你写成这样:

int factorial(int n) {
    if (n <= 1) {
        return 1;
    }
    return n * factorial(n);  // 没减1!死循环
}

那就等着段错误吧。

💡 小技巧:写递归函数时,我习惯先写终止条件,再写递归逻辑。这样思路更清晰,也不容易漏掉终止条件。

1.4 调试递归的实用技巧

递归出问题了怎么查?我分享几个实战经验:

技巧 说明 适用场景
加打印日志 在函数入口打印参数值 快速定位递归深度
限制递归深度 加一个全局计数器,超过阈值就报错 防止无限递归拖垮系统
用GDB调试 设置断点,观察栈帧变化 复杂递归逻辑
小规模测试 先用小数据跑,验证逻辑 所有递归函数

我个人最常用的是加打印日志。比如:

int factorial(int n) {
    printf("factorial(%d) 被调用\n", n);  // 调试用
    if (n <= 1) {
        return 1;
    }
    return n * factorial(n - 1);
}

跑一下就能看到递归的调用顺序。如果发现某个参数值一直不变,那肯定有问题。

1.5 递归深度与栈溢出

就算你写对了终止条件,递归深度太大也会出问题。

Linux默认的栈空间一般是8MB。每次函数调用大概占几十到几百字节。算一下:8MB / 100字节 ≈ 80000次调用。看起来挺多?但如果你递归处理一个10万长度的链表,直接就爆了。

我遇到过这种情况:一个同事用递归遍历超长链表,测试数据小的时候没问题,一上生产数据就崩。排查了半天,最后发现是递归深度太大导致栈溢出。

解决方案:
  • 对于深度不确定的递归,考虑改用循环(迭代)实现
  • 或者用尾递归优化(但C编译器不一定支持)
  • 实在要用递归,可以手动增大栈空间(不推荐,治标不治本)

1.6 知识体系总览

下面这张图帮你理清本章的核心逻辑:

递归的陷阱 为什么会发生? 缺少终止条件 参数不向终止条件靠近 非法输入 递归深度过大 后果是什么? 栈空间耗尽 段错误 / 栈溢出 程序崩溃 如何避免? 明确终止条件 参数逐步逼近终止 检查输入合法性 控制递归深度 调试技巧 加日志 → 限制深度 → GDB调试 → 小规模测试

1.7 写在最后

递归本身并不可怕。可怕的是你以为写对了,实际上没有。

我现在的习惯是:每写一个递归函数,先问自己三个问题——

  • 终止条件写了吗?
  • 参数在变化吗?
  • 输入数据合法吗?

这三个问题过一遍,90%的递归陷阱都能避开。

至于剩下的10%...嗯,那就靠调试技巧了。加日志、限制深度、小规模测试,三板斧下去,基本都能搞定。

记住一句话:递归是工具,不是目的。该用的时候用,不该用的时候别硬上。有时候一个简单的循环,比递归靠谱得多。


公众号:蓝海资料掘金营,微信deep3321