第五章 函数与 lambda:把代码打包复用

← 返回目录

本章目标:函数的声明定义、参数传递、返回值、重载、 以及现代 C++ 的灵魂——lambda 表达式。

5.1 函数:一段有名字的代码

int add(int a, int b) {   // 返回类型 名字(参数)
    return a + b;         // return 返回结果
}

int main() {
    int s = add(3, 5);    // 调用:s == 8
}

要点:

  void say_hi() { std::println("hi"); }

5.2 先声明还是先定义?

C++ 要求"先看到再用"。函数可以: 1) 先定义再调用(简单,教程一直这么干) 2) 先声明后定义(大项目必须):

   int add(int a, int b);   // 声明:告诉编译器"有这么个函数"

   int main() { return add(1, 2); }   // 此时可以用

   int add(int a, int b) { return a + b; }  // 定义在文件后面

"声明"= 只写签名不写身体,类似"合同"; "定义"= 签名 + 实现,类似"履约"。

5.3 参数传递:传值 vs 传引用(重点!)

1) 传值(拷贝):函数拿到的是副本,改它不影响外面

   void f(int x) { x = 999; }       // 外面不受影响

2) 传引用(&):函数拿到的是"本人",改它影响外面

   void f(int& x) { x = 999; }      // 外面变了!

3) 传 const 引用:既要避免拷贝(性能),又不允许修改

   void print_big(const std::string& s) { ... }

经验法则(重要,背下来):

为什么大对象不要传值?因为每次调用都完整复制一份, string 几 MB 就复制几 MB,又慢又费内存。

5.4 默认参数

void greet(std::string name, std::string greeting = "你好") {
    std::println("{}, {}", greeting, name);
}
greet("小明");               // 你好, 小明
greet("小明", "早上好");      // 早上好, 小明

规则:默认参数只能从右往左连续给(greeting 有默认值, 它左边的 name 可以有默认值,但"跳过一个"不行)。

5.5 函数重载:同名不同参

int    max(int a, int b)          { return a > b ? a : b; }
double max(double a, double b)    { return a > b ? a : b; }
int    max(int a, int b, int c)   { return max(max(a,b), c); }

编译器根据"参数个数和类型"自动挑合适的版本。 这是 C++ 多态的第一种形态(编译期多态)。

注意:只改返回类型不算重载,编译报错。

5.6 lambda:就地写函数(现代 C++ 的灵魂)

lambda 就是"没有名字的临时函数",写在哪用在哪:

auto f = [](int x) { return x * 2; };   // 定义
int r = f(21);                           // 调用,r == 42

语法拆解:

[捕获] (参数) -> 返回类型 { 函数体 }
^^^^^^  ^^^^^^   ^^^^^^^^   ^^^^^^^^

捕获列表 参数表 返回类型 函数体

返回类型可以省略(编译器推断):

auto f = [](int x) { return x * 2; };

5.7 捕获:lambda 用外面的变量

默认情况下,lambda 不能使用外面的变量。要使用必须"捕获":

int offset = 10;
auto f = [offset](int x) { return x + offset; };  // 按值捕获
auto g = [&offset](int x) { offset += 1; return x; }; // 按引用捕获

auto all1 = [=](...){...};   // 全部按值捕获(旧风格,慎用)
auto all2 = [&](...){...};   // 全部按引用捕获
auto mix  = [=, &x]{...};    // 除了 x 按引用,其余按值
auto init = [y = offset*2](int x){ return x + y; }; // C++14 初始化捕获

人话解释"按值 vs 按引用":

注意:按引用捕获的变量必须活得比 lambda 久! lambda 存下来以后再用,而引用指向的变量已经销毁 → 悬空引用 → 崩溃。 这是新手最容易踩的内存 bug。

5.8 [C++17] 泛型 lambda:参数类型也让编译器猜

auto add = [](auto a, auto b) { return a + b; };
add(1, 2);       // 3
add(1.5, 2);     // 3.5
add(std::string("a"), "b");  // "ab"

参数写 auto 就能接受任意类型(模板的偷懒写法)。 注意:所有调用必须类型一致,add(1, 2.5) 会编译报错 (因为 a 被推断成 int 了)。

5.9 lambda 的实际用途(现在记住,后面是主角)

1) 传给 STL 算法(第十四章细讲):

   std::sort(v.begin(), v.end(), [](int a, int b){ return a > b; });

2) 作为回调:比如事件处理、线程任务 3) 就地写小逻辑,避免到处散落的具名小函数

5.10 return 的细节

  int& bad() { int x = 5; return x; }  // x 销毁了!别这么写

返回局部变量本身(按值)则是安全的,会自动拷贝。

本章小结

练习题


  1. 写函数 is_prime(int n) 判断素数,然后打印 2~100 的所有素数。
  2. 写一个 swap(int& a, int& b) 交换两个变量的值,验证传引用生效。
  3. 写一个 lambda,按值捕获一个 int,调用后验证原变量没变;

再改按引用捕获,验证变了。

  1. 写一个函数 print_vector(const std::vector<int>& v),

用范围 for 打印所有元素。

  1. 用 lambda + std::sort 把一个 vector 降序排列。