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、简单的数学运算。
还有一点:递归函数不能内联。编译器无法展开一个调用自身的函数——那会变成无限展开。另外,通过函数指针调用的函数通常也不会被内联,因为编译器在编译时不知道具体调用哪个函数。
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,递归太深就会栈溢出。
有些递归可以优化成尾递归——递归调用是函数的最后一个操作。某些编译器会对尾递归做优化,复用当前栈帧,避免栈溢出。但 C++ 标准不强制要求编译器做尾递归优化,所以别太依赖它。
6.5 知识体系总览
这一章的内容,我用一张图帮你理清关系:
这张图把四个知识点并列展示,方便你对比记忆。每个知识点都有它的适用场景和注意事项,没有哪个是「万能」的。
6.6 综合对比
最后,我用一个表格帮你快速回顾这四个概念的核心区别:
| 特性 | 默认参数 | 内联函数 | 函数指针 | 递归函数 |
|---|---|---|---|---|
| 核心作用 | 简化调用 | 减少调用开销 | 函数作为数据 | 分治解决问题 |
| 语法关键字 | = 默认值 | inline | * 和 函数签名 | 函数内调用自身 |
| 性能影响 | 无 | 可能提升,也可能膨胀 | 间接调用,略慢 | 栈开销大 |
| 常见坑 | 顺序错误、重复定义 | 大函数内联适得其反 | 语法复杂、类型不匹配 | 栈溢出、缺少终止条件 |
| 实际应用 | Qt 构造函数参数 | Qt getter/setter | qsort 回调、状态机 | 树遍历、分治算法 |
好了,这一章的内容就到这里。函数默认参数、内联、函数指针、递归,这四个概念单独看都不难,但放在一起容易混淆。你只要记住:默认参数是偷懒用的,内联是优化用的,函数指针是灵活用的,递归是解决特定问题用的。各司其职,别混用就行。
公众号:蓝海资料掘金营,微信deep3321