7. 函数进阶:默认参数、内联函数、函数指针、lambda表达式、递归函数

函数这东西,写几行代码就得打交道。但很多人写了几年,还在用最基础的那一套。说实话,C++ 的函数远不止「传参-返回」这么简单。今天我把几个进阶玩法掰开揉碎讲清楚,都是我实际项目中踩过坑、尝过甜头的东西。

7.1 默认参数:少写代码的偷懒技巧

默认参数,说白了就是给函数参数一个「保底值」。调用时你不传,它就自动用这个值。

void ShowLog(const std::string& msg, int level = 1) {
    std::cout << "[Level " << level << "] " << msg << std::endl;
}

// 调用
ShowLog("启动成功");           // level 默认为 1
ShowLog("严重错误", 5);        // 手动指定 level 为 5

我个人习惯把最常用的参数放在后面,这样默认值才有意义。你想想看,如果第一个参数就有默认值,后面全是必填,那调用时反而更别扭。

注意:默认参数只能从右向左连续声明。你不能写成 void f(int a = 1, int b),编译器会直接报错。

我在项目中遇到过一个问题:头文件里声明了默认参数,但实现文件里又写了一遍。结果两个地方不一致,编译不报错,运行结果却莫名其妙。后来我定了个规矩——默认参数只写在声明里,实现里绝不重复。

7.2 内联函数:用空间换时间

函数调用是有开销的——压栈、跳转、返回。如果一个函数体很小,比如就两三行,那调用开销可能比函数本身还大。这时候,inline 就派上用场了。

inline int Max(int a, int b) {
    return a > b ? a : b;
}

加了 inline,编译器会把函数体直接「粘贴」到调用处,省去调用开销。但注意,这只是个建议,不是命令。编译器觉得不合适,照样忽略你的 inline

经验之谈:内联函数适合 3-5 行的小函数。超过 10 行就别用了,代码膨胀反而拖慢速度。我曾经把一个 50 行的函数加了 inline,结果编译出来的二进制体积翻了一倍,性能反而下降了。

还有一点:inline 函数通常定义在头文件里。因为编译器需要在每个调用点看到完整的函数体,才能做替换。

7.3 函数指针:把函数当变量用

函数指针,就是指向函数的指针。你可以把它赋值、传参、甚至放进数组里。听起来有点绕,但用起来很灵活。

int Add(int a, int b) { return a + b; }
int Sub(int a, int b) { return a - b; }

int main() {
    // 声明一个函数指针
    int (*op)(int, int);
    
    op = Add;
    std::cout << op(10, 5) << std::endl;  // 输出 15
    
    op = Sub;
    std::cout << op(10, 5) << std::endl;  // 输出 5
}

函数指针的声明语法有点丑,我建议用 usingtypedef 给它起个别名:

using OpFunc = int(*)(int, int);
OpFunc op = Add;

我在项目中用函数指针做过一个简单的命令分发器。用户输入命令字符串,程序查表找到对应的处理函数,直接调用。比写一长串 if-else 清爽多了。

7.4 Lambda 表达式:匿名函数的优雅写法

Lambda 是 C++11 引入的,说白了就是「临时用一下的函数」。你不需要给它起名字,写出来直接用。

auto add = [](int a, int b) -> int {
    return a + b;
};
std::cout << add(3, 4) << std::endl;  // 输出 7

Lambda 的核心语法:[捕获列表](参数列表) -> 返回类型 { 函数体 }。捕获列表用来「抓取」外部变量:

int factor = 2;
auto multiply = [factor](int x) { return x * factor; };
std::cout << multiply(5) << std::endl;  // 输出 10
捕获方式:
  • [=] — 按值捕获所有外部变量
  • [&] — 按引用捕获所有外部变量
  • [x, &y] — 混合捕获,x 按值,y 按引用

我建议优先用按值捕获。按引用捕获容易出问题——如果 lambda 执行时,引用的变量已经销毁了,程序直接崩溃。我曾经在回调函数里吃过这个亏,排查了半天才发现是引用悬空了。

Lambda 在 STL 算法里特别好用:

std::vector<int> nums = {3, 1, 4, 1, 5, 9};
std::sort(nums.begin(), nums.end(), [](int a, int b) {
    return a > b;  // 降序排列
});

7.5 递归函数:自己调用自己

递归,就是函数调用自身。解决那些可以拆分成「相同子问题」的场景特别合适,比如遍历树、计算阶乘。

int Factorial(int n) {
    if (n <= 1) return 1;
    return n * Factorial(n - 1);
}

写递归要记住两件事:

  1. 终止条件 — 没有终止条件就是死循环,最终栈溢出崩溃
  2. 递推关系 — 当前结果怎么由子问题得到
避坑指南:递归深度太大会导致栈溢出。我曾经写过一个目录遍历函数,递归深度超过 1000 层,程序直接 segfault。后来改成了迭代 + 显式栈,问题解决。

递归的经典例子:斐波那契数列。但注意,直接递归效率极低,因为大量重复计算。用记忆化(缓存中间结果)或者直接改成循环,才是正经做法。

// 带记忆化的斐波那契
int Fib(int n, std::vector<int>& memo) {
    if (n <= 1) return n;
    if (memo[n] != -1) return memo[n];
    memo[n] = Fib(n-1, memo) + Fib(n-2, memo);
    return memo[n];
}

知识体系总览

下面这张图把本章的核心知识点串起来了。你可以看到,默认参数和内联函数偏向「编译期优化」,函数指针和 lambda 偏向「运行时灵活性」,递归则是另一种思维模式。

函数进阶 默认参数 • 从右向左连续声明 • 声明处写,实现处不写 • 常用参数放后面 内联函数 • 空间换时间 • 适合 3-5 行小函数 • 定义在头文件中 函数指针 • 指向函数的指针 • 用 using 简化声明 • 适合做分发器 Lambda 表达式 • 匿名函数,临时使用 • 捕获列表控制外部变量 • 配合 STL 算法极佳 递归函数 • 自己调用自己 • 必须有终止条件 • 注意栈溢出风险

这五个知识点,每个单独看都不难。但组合起来,你就能写出更灵活、更高效的代码。比如用 lambda 做回调、用函数指针做策略模式、用递归处理树形结构——这些都是实际项目里的高频场景。

我的建议:别急着一次性全用上。先把默认参数和内联函数用熟,再慢慢接触函数指针和 lambda。递归嘛,遇到树结构时自然就会了。

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