C++ 函数
函数(function)是组织代码的最小复用单元。把一段逻辑封装成函数,起个名字,就能反复调用。这一章我们看 C++ 函数的方方面面:参数传递方式、默认参数、函数重载、内联、递归,以及 C++11 引入的革命性特性——lambda 表达式。
一、函数的基本结构
函数由返回类型、名字、参数列表、函数体组成:
#include <iostream>
using namespace std;
// 函数声明(原型):告诉编译器有这个函数
int add(int a, int b);
int main() {
int sum = add(3, 5); // 函数调用
cout << sum << endl; // 8
return 0;
}
// 函数定义:具体实现
int add(int a, int b) {
return a + b; // 函数体
}
// 函数签名:返回类型 + 名字 + 参数列表
// int add(int, int) 和 double add(int, int) 是不同签名- 声明(原型):告诉编译器"有这么个函数",可以写在调用之前。
- 定义:具体实现。声明和定义可以分开(常见于头文件 + 源文件)。
- 返回类型:函数返回值的类型。不返回用
void。 - 参数列表:形参的类型和名字。无参写
()或(void)。
二、参数传递:值 vs 引用(C++ 重点)
C++ 函数参数有三种传递方式,这是 C++ 区别于 Java/Python 的核心:
#include <iostream>
using namespace std;
// 1. 值传递:形参是实参的副本,改了不影响外面
void byValue(int x) {
x = 100;
}
// 2. 引用传递:形参是实参的别名,改了直接作用原变量
void byReference(int& x) {
x = 100;
}
// 3. const 引用:不修改,但避免拷贝(大对象必备)
void print(const string& s) {
cout << s << endl; // 只读,不能改 s
}
int main() {
int a = 1;
byValue(a);
cout << a << endl; // 1(没变)
int b = 1;
byReference(b);
cout << b << endl; // 100(被修改了)
string big = "very long string ...";
print(big); // 用 const 引用,不拷贝
return 0;
}- 值传递:形参是实参的副本,函数内修改形参不影响外面。适合小型内置类型(int、double)。
- 引用传递:形参是实参的别名,修改形参直接作用原变量。用于"输出参数"——让函数修改外部变量。
- const 引用:不修改,但避免拷贝。大对象(string、vector)必备,否则每次调用都拷贝一份,性能爆炸。
记住这条规则:只读小类型用值,只读大类型用 const 引用,要修改用引用。这是 C++ 性能优化的第一原则。
三、默认参数
给参数设默认值,调用时可以省略:
#include <iostream>
using namespace std;
// 默认参数:从右往左给,调用时可省略
void greet(string name, string greeting = "Hello", string punct = "!") {
cout << greeting << ", " << name << punct << endl;
}
int main() {
greet("Alice"); // Hello, Alice!
greet("Bob", "Hi"); // Hi, Bob!
greet("Carol", "Hey", "."); // Hey, Carol.
// greet("Dave", , "."); // 编译错误:不能跳过中间参数
// 默认参数必须在声明里写,不能在定义里再写
return 0;
}规则:
- 从右往左给:有默认值的参数右边必须都有默认值。
- 调用时不能跳过中间参数:
f(1, , 3)不行。 - 声明和定义只能一处写:声明里写了,定义里不能再写(否则编译错误)。
四、函数重载(overload)
C++ 允许同名函数只要参数列表不同就能共存,这叫重载:
#include <iostream>
using namespace std;
// 函数重载:同名函数,但参数列表不同
// 编译器根据调用时参数类型,自动选对那个
int add(int a, int b) {
return a + b;
}
double add(double a, double b) {
return a + b;
}
int add(int a, int b, int c) {
return a + b + c;
}
string add(string a, string b) {
return a + b;
}
int main() {
cout << add(1, 2) << endl; // 3 (调 int 版)
cout << add(1.5, 2.5) << endl; // 4 (调 double 版)
cout << add(1, 2, 3) << endl; // 6 (调三参数版)
cout << add("Hello, "s, "C++") << endl;// Hello, C++(调 string 版)
return 0;
}编译器根据参数个数/类型自动选对那个,这叫重载决议(overload resolution)。注意:
- 返回类型不同不算重载:
int f()和double f()不能共存(编译器不知道你要哪个)。 - const 形参有时算重载,如
f(int&)和f(const int&)。 - 别滥用:重载太多会让代码可读性下降,而且重载决议有歧义时编译会报错。
五、内联函数 inline
inline 是给编译器的建议:把函数体直接展开到调用处,省去函数调用开销:
#include <iostream>
using namespace std;
// inline:建议编译器"内联展开"(把函数体直接贴到调用处)
// 适合短小、频繁调用的函数,省去函数调用开销
inline int square(int x) {
return x * x;
}
// 现代 C++:在类内定义的成员函数自动 inline
// 编译器比人聪明,可能不展开你标了 inline 的函数,
// 也可能展开你没标 inline 的函数——inline 只是建议
int main() {
cout << square(5) << endl; // 25
// 编译后可能变成:cout << (5*5) << endl;
return 0;
}- 适合短小、频繁调用的函数(几行代码)。
- 不适合长函数:展开会让代码体积爆炸。
- 现代编译器很聪明:inline 只是建议,最终是否展开由编译器决定。
- 类内定义的成员函数自动 inline。
六、递归
函数调用自己就是递归。经典例子:
#include <iostream>
using namespace std;
// 递归:函数调用自己
// 必须有"终止条件",否则无限递归栈溢出
// 经典:阶乘 n! = n * (n-1)!
int factorial(int n) {
if (n <= 1) return 1; // 终止条件
return n * factorial(n - 1); // 递归调用
}
// 经典:斐波那契数列 F(n) = F(n-1) + F(n-2)
int fib(int n) {
if (n < 2) return n;
return fib(n - 1) + fib(n - 2);
}
int main() {
cout << factorial(5) << endl; // 120 (5*4*3*2*1)
cout << fib(10) << endl; // 55
// 注意:递归版 fib 是指数复杂度,大 n 时慢得离谱
// 实际用循环或记忆化
return 0;
}递归的两个关键:
- 终止条件:必须有,否则无限递归直到栈溢出(Stack Overflow)。
- 问题规模递减:每次递归调用都要让问题更接近终止条件。
递归的代价:每次调用都消耗栈空间,深度太大会栈溢出。如 fib 的朴素递归是指数复杂度,实际工程中往往改用循环或"记忆化递归"。
七、lambda 表达式(C++11)
lambda 是匿名函数,可以就地定义、就地使用。这是现代 C++ 最重要的特性之一:
#include <iostream>
#include <vector>
#include <algorithm>
using namespace std;
int main() {
// lambda:匿名函数,C++11 引入
// 语法: [捕获](参数) -> 返回类型 { 函数体 }
// 1. 基本 lambda
auto greet = []() {
cout << "Hello!" << endl;
};
greet(); // Hello!
// 2. 带参数和返回值
auto add = [](int a, int b) -> int {
return a + b;
};
cout << add(3, 4) << endl; // 7
// 3. 捕获外部变量
int factor = 10;
auto multiply = [factor](int x) { // 用值捕获 factor
return x * factor;
};
cout << multiply(5) << endl; // 50
// 4. STL 算法的回调(最常见用法)
vector<int> nums = {3, 1, 4, 1, 5, 9, 2, 6};
sort(nums.begin(), nums.end(), [](int a, int b) {
return a > b; // 降序排序
});
for (int x : nums) cout << x << " ";
cout << endl;
// 输出: 9 6 5 4 3 2 1 1
return 0;
}lambda 语法:方括号捕获 + 小括号参数 + 箭头返回类型 + 花括号函数体。
- 捕获列表
[]:决定 lambda 怎么访问外部变量。 - 参数列表
():和普通函数一样,可省略返回类型(让编译器推导)。 - 最常见用途:作为 STL 算法的回调(
sort、find_if、for_each等)。
八、lambda 捕获详解
#include <iostream>
using namespace std;
int main() {
int a = 1, b = 2, c = 3;
// [] 不捕获
// [=] 按值捕获所有外部变量
// [&] 按引用捕获所有外部变量
// [x] 只按值捕获 x
// [&x] 只按引用捕获 x
// [=, &x] 默认按值,但 x 按引用
// [&, x] 默认按引用,但 x 按值
auto f1 = [=]() { return a + b + c; }; // 值捕获,只读
cout << f1() << endl; // 6
auto f2 = [&]() { a = 100; b = 200; }; // 引用捕获,可改
f2();
cout << a << " " << b << endl; // 100 200
// C++14+:可以给捕获起别名、用初始化捕获
int x = 5;
auto f3 = [y = x + 10]() { return y; };
cout << f3() << endl; // 15
return 0;
}几个捕获陷阱:
- [=] 值捕获:捕获的是 lambda 创建时的快照,后面外部变量改了,lambda 里看到的还是旧值。
- [&] 引用捕获:要注意悬空引用——如果 lambda 比外部变量活得久(比如存到全局),外部变量销毁后 lambda 里访问就是未定义行为。
- this 捕获:在成员函数里写 lambda,想用类的成员要
[this]。
九、声明与定义的分离
实际项目里,函数声明和定义通常分到两个文件:
// 头文件 utils.h
#pragma once
int add(int a, int b); // 只有声明
// 源文件 utils.cpp
#include "utils.h"
int add(int a, int b) { // 定义
return a + b;
}
// 主文件 main.cpp
#include <iostream>
#include "utils.h" // 引入声明
using namespace std;
int main() {
cout << add(1, 2) << endl; // 3
return 0;
}
// 编译:g++ main.cpp utils.cpp -o app
// 为什么分离?让声明在头文件,其他文件 include 后就能用;
// 实现细节藏在 .cpp 里,加快编译速度、隐藏实现。- 头文件 .h:声明(原型)和类型定义。
- 源文件 .cpp:实现细节。
- 好处:加快编译(改动 .cpp 不会让其他文件重新编译)、隐藏实现、便于协作。
小结
这一章你掌握了 C++ 函数的全部要点:三种参数传递方式(值/引用/const 引用)、默认参数、函数重载、inline、递归、lambda。下一篇我们专门讲引用——C++ 独有的概念,以及它和指针的对比。
← 上一篇 C++ 控制流
下一篇 C++ 引用 →