C/C++ 入门:函数
学完变量、运算符和控制流之后,函数就是把一段逻辑整理成“可重复使用的小模块”。
📚 基本概念速读
| 名称 | 定义 | 省流 |
|---|---|---|
| 函数 | 一段有名字、可以被调用的代码 | 可复用逻辑 |
| 参数 | 调用函数时传进去的数据 | 输入 |
| 返回值 | 函数执行完交回来的结果 | 输出 |
| 函数声明 | 提前告诉编译器函数长什么样 | 先打招呼 |
| 函数定义 | 真正写出函数体 | 真正干活 |
| 值传递 | 把实参的值复制一份传给函数 | 传副本 |
| 函数重载 | 多个函数同名,但参数列表不同 | 同名不同参 |
| 递归 | 函数自己调用自己 | 自己拆问题 |
🧩 函数的基本模型
函数可以先粗略理解成一个加工盒子:给它输入,它执行一段逻辑,然后返回结果。
flowchart LR
A[参数] --> B[函数体]
B --> C[返回值]
最小例子:
1 |
|
这段代码里:
| 部分 | 示例 | 含义 |
|---|---|---|
| 返回类型 | int |
函数最后会返回一个整数 |
| 函数名 | add |
调用函数时使用的名字 |
| 参数列表 | int a, int b |
函数需要两个整数输入 |
| 函数体 | { return a + b; } |
函数真正执行的逻辑 |
| 函数调用 | add(3, 5) |
让函数运行一次 |
🛠️ 函数定义与调用
函数定义
函数定义的一般形式:
1 | 返回类型 函数名(参数列表) |
示例:
1 | int square(int x) |
square 接收一个整数 x,返回
x * x 的结果。
函数调用
调用函数时,要写函数名,并在括号里放入实际参数:
1 | int value = square(4); |
调用过程可以理解为:
1 | square(4) |
没有返回值的函数
如果函数只做一件事,不需要返回结果,可以使用 void:
1 | void printLine() |
void
表示“没有返回值”。这种函数可以直接调用,不需要接收结果。
🧾 函数声明与头文件前置认识
为什么需要函数声明
C/C++ 编译器通常从上到下读取代码。如果在 main
里调用一个后面才定义的函数,编译器可能还不知道这个函数存在。
例如:
1 |
|
这时可以在前面加一个函数声明:
1 |
|
int add(int a, int b);
这一行就是声明。它告诉编译器:
后面会有一个叫
add的函数,它接收两个int,并返回一个int。
声明和定义的区别
| 名称 | 是否有函数体 | 作用 |
|---|---|---|
| 函数声明 | 没有 | 告诉编译器函数接口 |
| 函数定义 | 有 | 写出函数实际逻辑 |
1 | int add(int a, int b); // 声明 |
头文件前置认识
实际项目里,函数声明常放在头文件里,比如
math_utils.h:
1 | // math_utils.h |
函数定义放在 .cpp 文件里:
1 | // math_utils.cpp |
使用函数的地方包含头文件:
1 |
|
初学阶段先记住一句话:头文件通常放声明,源文件通常放定义。
📦 参数传递
值传递
最常见的是值传递。函数拿到的是实参的一份副本,函数内部修改参数,不会影响外面的变量。
1 |
|
change(a) 调用时,a 的值被复制给
x。修改 x 不等于修改 a。
可以这样理解:
1 | a = 10 |
指针参数预告
如果希望函数能修改外部变量,C 语言里常用指针参数:
1 | void change(int *p) |
这里的 p 保存的是变量地址,*p
表示通过地址找到原来的变量。指针涉及地址和内存,后面会单独展开。
引用参数预告
C++ 里还常用引用参数:
1 | void change(int &x) |
引用可以先理解为“给原变量起了一个别名”。函数里改
x,外面的变量也会被改。引用也会在后续文章里详细讲。
三种传参方式先对比
| 方式 | 示例 | 是否能修改外部变量 | 初学阶段理解 |
|---|---|---|---|
| 值传递 | int x |
否 | 传副本 |
| 指针参数 | int *p |
可以 | 传地址 |
| 引用参数 | int &x |
可以 | 传别名 |
🔁 返回值
返回普通值
最常见的返回值就是返回计算结果:
1 | int maxValue(int a, int b) |
return 会结束当前函数,并把后面的值交回调用者。
1 | int m = maxValue(3, 5); |
返回状态码
有些函数不只关心“结果是什么”,还关心“是否成功”。这时可以返回状态码。
1 | int divide(int a, int b, int &result) |
调用时:
1 | int result = 0; |
这里约定:
| 返回码 | 含义 |
|---|---|
0 |
成功 |
非 0 |
失败或不同错误类型 |
这种风格在 C 语言和系统编程里很常见。
避免返回局部对象地址
不要返回局部变量的地址:
1 | int* bad() |
原因是:x
是函数里的局部变量,函数结束后它的生命周期也结束了。返回
&x 等于把一个已经失效的位置交给外面使用。
可以粗略理解为:
1 | 进入 bad() |
初学阶段先记住:不要返回局部变量的指针或引用。
🧬 函数重载
同名函数
C++ 允许多个函数使用同一个名字,只要它们的参数列表不同。这叫函数重载。
1 | int add(int a, int b) |
调用时,编译器会根据参数类型选择合适的版本:
1 | cout << add(3, 5) << endl; // 调用 int 版本 |
参数匹配
重载主要看参数列表,包括参数个数和参数类型。
1 | void print(int x) |
下面三种调用会匹配不同函数:
1 | print(10); |
返回类型不能单独构成重载
只改返回类型,不改参数列表,不算合法重载:
1 | int getValue() |
原因是调用 getValue()
时,编译器只看调用形式,无法仅靠返回类型稳定判断应该选哪个函数。
二义性
如果多个重载版本都像是“差不多能匹配”,编译器可能报二义性错误。
1 | void show(long x) |
10 是 int,它既可以转换成
long,也可以转换成
double。类似场景可能让编译器难以选择。
避免二义性的简单办法:
| 做法 | 示例 |
|---|---|
| 让参数类型更明确 | show(10L); |
| 手动类型转换 | show(static_cast<double>(10)); |
| 不写过于接近的重载 | 避免 long 和 double 混在一起 |
🧠 递归基础
什么是递归
递归就是函数自己调用自己。它适合处理“一个大问题可以拆成更小的同类问题”的场景。
典型例子是阶乘:
1 | 5! = 5 * 4! |
代码:
1 | int factorial(int n) |
递归出口
递归必须有出口,也就是停止继续调用自己的条件。
1 | if (n == 1) { |
这就是阶乘函数的递归出口。如果没有出口,函数会一直调用自己,直到程序崩溃。
更稳一点的写法通常会处理 0:
1 | int factorial(int n) |
调用栈
每次函数调用,程序都会在调用栈里保存这次调用的现场。递归调用多次,就会一层一层压栈。
以 factorial(4) 为例:
1 | factorial(4) |
到达出口后,再一层一层返回:
1 | factorial(1) 返回 1 |
所以 factorial(4) 的结果是 24。
典型错误
| 错误 | 问题 | 修正 |
|---|---|---|
| 没有递归出口 | 无限递归 | 写清楚停止条件 |
| 递归没有逼近出口 | 参数不变或方向错 | 每次调用都让问题变小 |
| 递归层数太深 | 调用栈耗尽 | 改用循环或优化算法 |
| 忘记处理边界值 | 0、负数等输入异常 |
先判断边界 |
错误示例:
1 | int bad(int n) |
这个函数永远不会停止。
一个更合理的版本:
1 | int sumTo(int n) |
⚠️ 常见误区
| 误区 | 正解 |
|---|---|
| 函数声明和函数定义是一回事 | 声明只告诉接口,定义才有函数体 |
| 参数传进去后一定会修改原变量 | 值传递只修改副本 |
return 之后函数还会继续执行 |
return 会立刻结束当前函数 |
| 返回局部变量地址也能用 | 局部变量函数结束后失效,不能这样返回 |
| 函数重载只看函数名 | 还要看参数个数和参数类型 |
| 递归就是循环的高级写法 | 递归依赖问题拆分和调用栈,不是无脑替代循环 |
✅ 总结
函数的核心价值,是把一段逻辑封装成清晰、可复用、可组合的模块。
初学阶段先抓住四件事:函数怎么定义、怎么调用、参数怎么传、结果怎么返回。至于指针、引用、头文件拆分和递归优化,后面可以继续一层层展开。
Happy Hacking! 🎉