函数(Functions)
函数是将一段逻辑封装为可复用单元的核心机制。合理拆分函数能让代码结构清晰、易于测试和维护。
1. 为什么需要函数
没有函数,所有代码堆在 main 里:
int main()
{
// 100 行验证输入的代码
// 200 行计算逻辑
// 50 行输出结果
// ——难以阅读、难以修改、难以复用
}
拆分为函数后:
int main()
{
int input = get_valid_input();
int result = calculate(input);
print_result(result);
// ——意图一目了然
}
函数的核心价值:封装细节、命名意图、支持复用。
2. 函数的基本结构
返回类型 函数名(参数列表)
{
// 函数体
return 返回值; // 如果返回类型不是 void
}
// 一个完整的函数
int add(int a, int b)
{
int sum = a + b;
return sum;
}
| 组成部分 | 示例中的值 | 说明 |
|---|---|---|
| 返回类型 | int |
函数返回值的类型;不返回用 void |
| 函数名 | add |
描述函数做什么(动词或动词短语) |
| 参数列表 | int a, int b |
调用者传入的数据 |
| 函数体 | { ... } |
具体执行逻辑 |
| 返回值 | return sum; |
将结果传回调用者 |
3. 函数声明与定义
C 语言要求函数在使用前被编译器知晓。有两种方式:
方式一:定义在前(先写函数体,再调用)
#include <stdio.h>
// 定义在前
int add(int a, int b)
{
return a + b;
}
int main()
{
int result = add(3, 5); // 可以调用
printf("%d\n", result);
return 0;
}
方式二:声明在前,定义在后(推荐)
#include <stdio.h>
// 函数声明(原型)——只写签名,不写函数体
int add(int a, int b);
int main()
{
int result = add(3, 5); // OK,编译器已看到声明
printf("%d\n", result);
return 0;
}
// 函数定义——可以在后面,甚至可以在另一个 .c 文件中
int add(int a, int b)
{
return a + b;
}
声明中的参数名可省略(int add(int, int);),但保留名称有助于理解。
4. 参数传递
4.1 传值(Pass by Value)
C 语言默认传值——函数收到参数的副本,修改副本不影响原变量:
void increment(int x)
{
x = x + 1; // 只修改了局部副本
printf("函数内:%d\n", x);
}
int main()
{
int n = 10;
increment(n);
printf("函数外:%d\n", n); // 仍然是 10
return 0;
}
// 输出:
// 函数内:11
// 函数外:10
4.2 传指针(模拟传引用)
要让函数修改外部变量,需传递该变量的地址:
void increment(int* p)
{
*p = *p + 1; // 通过指针修改原值
}
int main()
{
int n = 10;
increment(&n); // 传入 n 的地址
printf("%d\n", n); // 11
return 0;
}
传值 vs 传指针对比:
| 传递方式 | 函数内修改 | 适用场景 |
|---|---|---|
传值 void f(int x) |
不影响原变量 | 只需要读取数据 |
传指针 void f(int* p) |
影响原变量 | 需要修改数据;传递大体积数据避免拷贝 |
4.3 数组作为参数
数组作为参数时退化为指针,传递的是首元素地址而非整个数组:
void print_array(int arr[], int size) // arr 实际上是一个指针
{
for (int i = 0; i < size; i++) {
printf("%d ", arr[i]);
}
}
因此必须额外传递数组的长度。在函数内部 sizeof(arr) 返回的是指针大小(通常 4 或 8 字节),而非数组实际大小。
5. 返回值
5.1 基本用法
int max(int a, int b)
{
if (a > b) return a;
else return b;
}
return 做了两件事:
1. 将返回值传递给调用者
2. 立即退出当前函数
int divide(int a, int b, int* result)
{
if (b == 0) {
return 0; // 失败立即返回,后续代码不执行
}
*result = a / b;
return 1; // 成功
}
5.2 无返回值:void
void greet(const char* name)
{
printf("你好,%s!\n", name);
// 可以写 return; 但不必要;函数末尾自动返回
}
6. 递归
函数调用自身称为递归,适合处理具有自相似结构的问题。
6.1 阶乘示例
int factorial(int n)
{
if (n <= 1) {
return 1; // 基准条件(base case)
}
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
6.2 递归的两个必备要素
| 要素 | 说明 | 缺失的后果 |
|---|---|---|
| 基准条件(base case) | 停止递归的出口 | 无限递归,最终栈溢出 |
| 递归条件 | 向基准条件推进 | 同上 |
6.3 递归的代价
每次函数调用都会消耗栈空间(存储局部变量、返回地址等)。递归层数过深会导致栈溢出(Stack Overflow)。对于简单的循环问题,迭代(for/while)通常比递归更高效。
7. 函数设计原则
| 原则 | 说明 | 反面示例 |
|---|---|---|
| 单一职责 | 一个函数只做一件事 | calculate_and_print_and_save() |
| 命名表意图 | 函数名描述做什么,而非怎么做的 | procData() → 应改为 filter_valid_records() |
| 参数宜少 | 超过 4 个参数考虑封装为结构体 | void draw(int, int, int, int, int, int, char*) |
| 无副作用 | 能不修改外部状态就不修改 | get_count() 内部却修改了全局变量 |
| 长度适中 | 一屏内(约 20-50 行)易于理解 | 200 行的单一函数 |
8. 变量作用域与函数
int global = 100; // 全局变量——所有函数可见
void foo()
{
static int call_count = 0; // 静态局部变量——值在多次调用间保持
call_count++;
printf("foo 被调用了 %d 次\n", call_count);
int local = 10; // 局部变量——每次调用重新创建
}
int main()
{
foo(); // foo 被调用了 1 次
foo(); // foo 被调用了 2 次
foo(); // foo 被调用了 3 次
return 0;
}
关于存储类的完整讨论,详见《存储类与作用域》。
9. 常见错误
| 错误 | 问题 | 正确做法 |
|---|---|---|
| 忘记函数声明 | 调用在定义之前却没有前置声明 | 在文件顶部或头文件中添加函数原型 |
| 返回局部变量地址 | return &x; 中 x 是局部变量,函数返回后其内存已失效 |
返回值本身(return x),或用 static/动态分配 |
| 数组参数丢失大小信息 | void f(int arr[]) 内部用 sizeof(arr) |
额外传递 size 参数 |
| 递归无基准条件 | 无限递归导致栈溢出 | 确保每次递归都向基准条件靠近 |
| 修改传值参数 | 期望 f(x) 后 x 被修改 |
改用指针参数 f(&x) |
10. 总结
| 要点 | 说明 |
|---|---|
| 函数声明 | 调用前让编译器知晓函数签名(返回类型 + 名称 + 参数类型) |
| 默认传值 | 函数内修改参数不影响原变量 |
| 需要修改用指针 | void f(int* p) 传入地址以修改外部变量 |
| 数组参数退化 | 数组作为参数时退化为指针,需额外传长度 |
void 无返回值 |
用于纯副作用的函数 |
| 递归需基准条件 | 否则导致栈溢出 |
| 返回局部变量地址是危险行为 | 函数返回后该地址的内存已无效 |
作者: Qing | 修改日期: 2026-07-18