为什么需要函数
三个原因:
1. 复用:写一次用多次,不用每次重写
2. 组织代码:给逻辑起名字,calculateArea() 比"第 47-62 行"好找得多
3. 隔离变量:函数里定义的变量不会跟外面的同名变量冲突
函数的基本结构
返回类型 函数名(参数列表) {
// 函数体
return 返回值;
}
几种形式:
// 无参数,无返回值
void sayHello() {
printf("Hello!\\n");
}
// 有参数,有返回值
int add(int a, int b) {
return a + b;
}
// 有参数,无返回值
void printSum(int a, int b) {
printf("%d\\n", a + b);
}
void 表示没有返回值
参数列表为空可以写 void 或不写:void f() 或 void f(void)
参数传递:值传递
C 语言只有值传递——函数收到的是参数的副本,不是原变量本身。
void tryToChange(int x) {
x = 100; // 只改了副本
}
int main() {
int a = 5;
tryToChange(a);
printf("%d\\n", a); // 还是 5,没变
return 0;
}
想在函数里修改外部变量怎么办?用指针——传地址进去。
返回值
return 结束函数并把值带回
函数类型是 void 时可以省略 return,或写 return;(不带值)
一个函数可以有多个 return(比如 if-else 各一个)
int max(int a, int b) {
if (a > b) return a;
return b;
}
注意:不写 return 时,返回值是未定义的(垃圾值),不要依赖它。
声明 vs 定义
这是两个容易混淆的概念:
| 声明(原型) | 定义 | |
| 内容 | 只有函数签名 | 签名 + 函数体 |
| 结尾 | 分号 ; | 花括号 {} |
| 作用 | 告诉编译器函数长什么样 |
函数的实际实现 |
// 声明
int add(int a, int b);
// 定义
int add(int a, int b) {
return a + b;
}
判断方法:看有没有 {},不看放在什么位置。
为什么需要声明?
C 语言编译器从上往下扫描代码。如果函数定义在 main 后面,编译器遇到 add(3, 4) 时还不知道 add 是什么——报错。
两种解决方式:
1. 把函数定义写在 main 前面
2. 在 main 前面加声明(函数原型)
实际项目中推荐方式 2,把声明放头文件(.h),定义放源文件(.c)。
作用域
变量有作用域——在哪个 {} 里定义的,就只在那个 {} 里有效。
int x = 10; // 全局变量——整个文件都能用
void foo() {
int y = 20; // 局部变量——只在 foo 里有效
printf("%d %d\\n", x, y); // 能访问 x 和 y
}
int main() {
printf("%d\\n", x); // 能访问 x
// printf("%d\\n", y); // 编译报错!y 不在这里
foo();
return 0;
}
局部变量遮蔽全局变量
int x = 10;
void foo() {
int x = 20; // 这个 x 遮蔽了全局的 x
printf("%d\\n", x); // 输出 20,不是 10
}
建议
少用全局变量。 任何函数都能改全局变量,出了 bug 很难定位是哪个函数改的。
递归
函数可以调用自己——这叫递归。
int factorial(int n) {
if (n <= 1) return 1; // 基线条件
return n * factorial(n – 1); // 递归调用
}
执行过程(以 factorial(4) 为例):
factorial(4)
= 4 × factorial(3)
= 4 × 3 × factorial(2)
= 4 × 3 × 2 × factorial(1)
= 4 × 3 × 2 × 1
= 24
递归的两个必要条件
1. 基线条件(停止条件):什么时候不再调用自己
2. 递归步骤:每次调用必须向基线条件靠近
没有基线条件 → 无限递归 → 栈溢出(stack overflow)。
递归VS循环
| 递归 | 循环 | |
| 效率 | 每次调用消耗栈空间 | 不额外消耗栈空间 |
| 可读性 | 某些问题更直观(树、分治) | 简单问题更直观 |
| 适用 | 树遍历、分治、回溯 | 已知次数的重复操作 |
大多数场景用循环更高效,但某些问题用递归更自然。
常见误区速查
| 误区 | 正确理解 |
| 函数能修改外部变量 | 值传递,改的是副本,不影响原变量 |
| 不写 return 也能拿到正确值 | 值传递,改的是副本,不影响原变量 |
| 不写 return 也能拿到正确值 | 返回值是未定义的垃圾值,不要依赖 |
| 声明和定义看位置 | 看有没有 {},有是定义,没有是声明 |
| 全局变量好用就多用 | 少用,任何函数都能改,出 bug 难定位 |
| 递归不需要基线条件 | 必须有,否则栈溢出 |
总结
函数 = 复用 + 组织 + 隔离变量
C 语言只有值传递,函数拿到的是副本
声明只有签名加 ;,定义有 {},判断看有没有花括号
局局部变量只在函数内有效,会遮蔽同名全局变量
递归必须有基线条件,否则栈溢出
