6、函数(下):函数默认参数、内联函数、函数指针、递归函数

好,咱们接着聊函数。上一章我们把函数的基本用法、重载、传参方式都捋了一遍。这一章要讲的几个概念,说实话,在刚入门时容易让人犯迷糊——默认参数、内联、函数指针、递归。它们各自解决不同的问题,但都是实际开发中绕不开的东西。

我个人习惯把这一章叫作「函数的进阶玩法」。你想想看,函数不只是写一段逻辑然后调用它,它还能有默认值、能建议编译器优化、能像变量一样被传递、还能自己调用自己。嗯,听起来有点意思吧?

6.1 函数默认参数

先说说默认参数。说白了,就是给函数的形参一个「保底值」。调用时如果你没传这个参数,编译器就自动用默认值顶上。

void showMessage(const QString &msg, int repeat = 1) {
    for (int i = 0; i < repeat; ++i) {
        qDebug() << msg;
    }
}

// 调用
showMessage("Hello");          // 等价于 showMessage("Hello", 1);
showMessage("Hi", 3);          // 打印三次

这里有个坑,我曾经踩过:默认参数必须从右向左连续提供。你不能写成 void func(int a = 10, int b);,编译器会直接报错。为什么?因为调用时传参是从左往右匹配的,如果左边有默认值右边没有,编译器就不知道第二个参数该不该用默认值。

注意:默认参数在声明和定义中只能出现一次。通常放在声明里,定义里不要重复写。否则编译器会报「重定义默认参数」的错误。

另外,默认参数的值在编译期就确定了。如果你写 void func(int x = getValue());,这个 getValue() 只在编译时调用一次,而不是每次调用函数时都重新计算。这一点和某些动态语言不一样,别搞混了。

6.2 内联函数

内联函数,关键字是 inline。它的作用是建议编译器:把函数体的代码直接插入到调用处,避免函数调用的开销(压栈、跳转、返回等)。

inline int add(int a, int b) {
    return a + b;
}

int result = add(3, 5);  // 编译器可能直接替换为 int result = 3 + 5;

你可能会问:那把所有函数都加上 inline 不就好了?别急。内联是一把双刃剑。

我在项目中遇到过这样的情况:一个团队新人把一个大函数(几百行)加了 inline,结果编译出来的可执行文件体积暴涨,而且运行速度反而没提升。为什么?因为内联会增加代码膨胀,导致 CPU 指令缓存命中率下降。说白了,内联适合短小、频繁调用的函数,比如 getter/setter、简单的数学运算。

经验之谈:现代编译器比你聪明。就算你不写 inline,编译器也可能自动内联;你写了 inline,编译器也可能忽略。所以 inline 更像是一个「建议」,而不是强制命令。在 Qt 源码里,很多短函数都加了 inline,比如 QRect 的 width()、height() 这些。

还有一点:递归函数不能内联。编译器无法展开一个调用自身的函数——那会变成无限展开。另外,通过函数指针调用的函数通常也不会被内联,因为编译器在编译时不知道具体调用哪个函数。

6.3 函数指针

函数指针,就是把函数当作一个变量来用。你可以把它赋值、传参、甚至放进数组里。这在实现回调、策略模式、状态机时非常有用。

// 定义一个函数指针类型
typedef int (*MathOp)(int, int);

// 几个具体函数
int add(int a, int b) { return a + b; }
int sub(int a, int b) { return a - b; }
int mul(int a, int b) { return a * b; }

// 使用函数指针
MathOp op = add;
int result = op(10, 5);  // 15

op = mul;
result = op(10, 5);      // 50

我记得刚学函数指针时,觉得语法特别别扭。尤其是 int (*p)(int, int) 这种写法,括号一多就晕。后来我总结了一个窍门:从变量名开始,先看右边再看左边。比如 int (*p)(int, int),p 先跟 * 结合,说明 p 是一个指针;然后跟 (int, int) 结合,说明它指向一个函数,这个函数接受两个 int 参数;最后看左边的 int,说明返回值是 int。

在 Qt 里,函数指针用得不算多,因为 Qt 有信号槽机制。但在 C 风格的回调、或者需要高性能的场景下,函数指针还是很有用的。比如 qsort() 的比较函数参数,就是函数指针。

核心要点:函数指针的类型由「返回值类型」和「参数列表」共同决定。两个函数如果返回值相同、参数列表也相同,就可以用同一个函数指针类型指向它们。

6.4 递归函数

递归,就是函数调用自身。听起来有点绕,但用对了地方非常优雅。典型的例子:计算阶乘、遍历树形结构、快速排序。

// 计算 n 的阶乘
int factorial(int n) {
    if (n <= 1) {
        return 1;  // 终止条件
    }
    return n * factorial(n - 1);  // 递归调用
}

递归有两个关键要素:终止条件递推关系。没有终止条件就是死循环,最终栈溢出。我在项目中调试过一个递归 bug,就是因为忘记写终止条件,程序跑了几秒后直接崩溃——调用栈深到几千层,把栈空间撑爆了。

递归的优点是代码简洁、逻辑清晰。缺点也很明显:性能开销大。每次递归调用都要压栈、传参、返回,比循环慢得多。而且递归深度受限于栈大小,一般默认栈空间只有几 MB,递归太深就会栈溢出。

避坑指南:我曾经在解析 JSON 时用了递归,结果遇到一个嵌套 1000 层的 JSON 文件,程序直接崩溃。后来改成了用栈模拟的迭代版本,问题解决。所以,如果递归深度不确定或者可能很大,优先考虑迭代。

有些递归可以优化成尾递归——递归调用是函数的最后一个操作。某些编译器会对尾递归做优化,复用当前栈帧,避免栈溢出。但 C++ 标准不强制要求编译器做尾递归优化,所以别太依赖它。

6.5 知识体系总览

这一章的内容,我用一张图帮你理清关系:

函数进阶 默认参数 内联函数 函数指针 递归函数 特点 • 从右向左连续提供 • 声明/定义只出现一次 • 编译期确定值 • 减少调用时代码量 • 适合可选参数场景 例: void f(int a, int b=0) 特点 • 建议编译器展开 • 避免函数调用开销 • 适合短小频繁函数 • 可能增加代码体积 • 递归/指针不可内联 例: inline int add(int,int) 特点 • 函数作为变量使用 • 支持赋值、传参 • 实现回调机制 • 类型由返回值+参数决定 • 可用于策略模式 例: int (*p)(int,int) 特点 • 函数调用自身 • 必须要有终止条件 • 代码简洁逻辑清晰 • 性能开销较大 • 深度过大会栈溢出 例: int fact(int n)

这张图把四个知识点并列展示,方便你对比记忆。每个知识点都有它的适用场景和注意事项,没有哪个是「万能」的。

6.6 综合对比

最后,我用一个表格帮你快速回顾这四个概念的核心区别:

特性 默认参数 内联函数 函数指针 递归函数
核心作用 简化调用 减少调用开销 函数作为数据 分治解决问题
语法关键字 = 默认值 inline * 和 函数签名 函数内调用自身
性能影响 可能提升,也可能膨胀 间接调用,略慢 栈开销大
常见坑 顺序错误、重复定义 大函数内联适得其反 语法复杂、类型不匹配 栈溢出、缺少终止条件
实际应用 Qt 构造函数参数 Qt getter/setter qsort 回调、状态机 树遍历、分治算法

好了,这一章的内容就到这里。函数默认参数、内联、函数指针、递归,这四个概念单独看都不难,但放在一起容易混淆。你只要记住:默认参数是偷懒用的,内联是优化用的,函数指针是灵活用的,递归是解决特定问题用的。各司其职,别混用就行。


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