C 函数
函数是 C 程序组织代码的基本单元——把一段逻辑打包,起个名字,需要时调用。好的函数职责单一、命名清晰、可复用。本篇带你搞懂 C 函数的定义、声明、参数传递(值传递与指针传递)、递归,以及一个新手必踩的坑:为什么 C 函数改不了外面的变量。
一、函数定义
C 函数由四部分组成:返回类型 + 函数名 + 参数列表 + 函数体。
#include <stdio.h>
// 函数定义:返回类型 + 名字 + 参数列表 + 函数体
int add(int a, int b) {
return a + b; // return 返回结果,函数结束
}
// 没有返回值用 void
void greet(void) { // void 表示无参数
printf("你好!\n");
}
// 参数也可以写 void 省略,但显式写 void 更规范
void say_hi() { // C 风格:空 () 表示"任意参数"(不推荐)
printf("Hi!\n");
}
int main(void) {
int sum = add(3, 5); // 调用函数,接收返回值
printf("3 + 5 = %d\n", sum);
greet(); // 调用无返回值函数
say_hi();
return 0;
}几个要点:
- 返回类型:函数计算结果的类型。无返回值用
void。C89 里省略返回类型默认是 int(不推荐),C99 起必须显式写。 - 参数列表:无参数写
void(规范),空()在 C 里表示"任意参数"(C++ 里才表示无参)。 - return:返回结果并结束函数。void 函数可以写
return;提前退出。
二、函数声明(原型)
C 编译器是从上到下读代码的,如果一个函数定义在 main 后面,main 里调用它编译器会报错"未声明的标识符"。解决办法是在前面写函数原型(只声明签名,不写函数体):
#include <stdio.h>
// 函数原型(声明):写在 main 前面,告诉编译器"有这个函数"
// 这样 main 可以调用定义在后面的函数
int add(int a, int b); // 末尾的分号!参数名可省略
double average(int, int); // 只写类型也行
int main(void) {
printf("%d\n", add(3, 5));
printf("%.2f\n", average(10, 20));
return 0;
}
// 真正的定义在后面
int add(int a, int b) {
return a + b;
}
double average(int a, int b) {
return (a + b) / 2.0;
}
// 小贴士:多文件项目里,函数原型写在头文件 .h
// 定义写在 .c,使用时 #include 头文件即可函数原型的作用是告诉编译器:"存在这样一个函数,长这样,具体实现后面会给你"。这样编译器就能做类型检查——你调用时参数类型不对,它会报错。
多文件项目里,函数原型通常写在头文件(.h)里,实现写在 .c 里。用的时候 #include 头文件即可——这就是标准库 stdio.h 在做的事。
三、值传递(C 的核心规则!)
这是新手最容易困惑的一点:C 的函数参数默认都是值传递——把实参的值拷贝一份给形参,函数内改形参不影响外面的实参。
#include <stdio.h>
// C 的参数默认是"值传递"(拷贝一份)
void try_modify(int x) {
x = 999; // 改的是拷贝,不影响外面的变量
}
// 要修改外部变量,必须传指针
void modify(int *p) {
*p = 999; // 通过指针改外面的变量
}
int main(void) {
int n = 1;
try_modify(n);
printf("n = %d\n", n); // 1,没变
modify(&n); // 传地址
printf("n = %d\n", n); // 999,被修改了
return 0;
}为什么会这样?因为函数内部的 x 是一份拷贝,它和外面的 n 是两个完全独立的变量(在不同的内存位置)。函数返回时 x 就销毁了。
要修改外部变量,必须传指针——把变量的地址传进去,函数内通过解引用 *p 修改的就是外面的变量本身。这是 C 没有"引用"(reference)机制的代价,也是 C 最经典的设计模式。
四、用指针"返回多个值"
C 函数只能返回一个值。如果你需要"返回多个结果"(比如除法的商和余数),经典做法是传指针作为输出参数:
#include <stdio.h>
// C 函数只能返回一个值
// 要"返回多个结果",经典做法:传入指针作为输出参数
void divmod(int a, int b, int *quotient, int *remainder) {
*quotient = a / b;
*remainder = a % b;
}
// 另一种做法:返回结构体(后续 struct 篇讲)
// 还可以用全局变量,但不推荐(破坏封装)
void swap(int *a, int *b) {
int tmp = *a;
*a = *b;
*b = tmp;
}
int main(void) {
int q, r;
divmod(17, 5, &q, &r); // 传 q、r 的地址
printf("17/5 = %d 余 %d\n", q, r); // 3 余 2
int x = 10, y = 20;
swap(&x, &y);
printf("x=%d, y=%d\n", x, y); // x=20, y=10
return 0;
}这种写法在 C 标准库里随处可见——scanf 就是这么工作的,所以你写 scanf("%d", &n) 必须带 &,否则它没法把读到的值写进 n。交换两个变量的 swap 函数也是经典例子,必须传指针才能交换成功。
五、递归
递归是函数调用自己。它能优雅地解决"自相似"问题(阶乘、斐波那契、树遍历),但性能不如循环(函数调用有开销,栈空间有限)。
#include <stdio.h>
// 递归:函数调用自己
// 经典1:阶乘 n! = n * (n-1)!
long factorial(int n) {
if (n <= 1) return 1; // 基准情形(必须有,否则无限递归)
return n * factorial(n - 1);
}
// 经典2:斐波那契 F(n) = F(n-1) + F(n-2)
long fib(int n) {
if (n < 2) return n;
return fib(n - 1) + fib(n - 2);
}
// 经典3:汉诺塔
void hanoi(int n, char from, char to, char via) {
if (n == 1) {
printf("%c -> %c\n", from, to);
return;
}
hanoi(n - 1, from, via, to);
printf("%c -> %c\n", from, to);
hanoi(n - 1, via, to, from);
}
int main(void) {
printf("5! = %ld\n", factorial(5)); // 120
printf("fib(10) = %ld\n", fib(10)); // 55
hanoi(3, 'A', 'C', 'B');
return 0;
}写递归的两条铁律:
- 必须有基准情形(base case):一个不再递归的条件,作为递归的终点。没有基准情形会无限递归,直到栈溢出崩溃。
- 每次递归要朝基准情形靠近:比如阶乘里
factorial(n-1)让 n 不断变小,最终会到 1。
注意示例里的斐波那契是低效的——它计算了大量的重复子问题(fib(5) 会算 fib(4)+fib(3),fib(4) 又算 fib(3)+fib(2),fib(3) 被算了两次)。实际项目里要么用循环,要么用记忆化(把算过的结果存起来)。
六、可变参数函数
C 支持定义"参数个数不确定"的函数,最典型的就是 printf。它通过 <stdarg.h> 提供的四个宏实现:
#include <stdio.h>
#include <stdarg.h> // 可变参数需要的头文件
// 可变参数函数:参数个数不确定
// 用 va_list / va_start / va_arg / va_end 四个宏
int sum_n(int count, ...) {
va_list args;
va_start(args, count); // count 是最后一个具名参数
int total = 0;
for (int i = 0; i < count; i++) {
total += va_arg(args, int); // 每次取一个 int
}
va_end(args); // 清理
return total;
}
// 自己实现一个简化版 printf 风格的函数
void log_ints(const char *tag, int count, ...) {
va_list args;
va_start(args, count);
printf("%s: ", tag);
for (int i = 0; i < count; i++) {
printf("%d ", va_arg(args, int));
}
printf("\n");
va_end(args);
}
int main(void) {
printf("%d\n", sum_n(3, 10, 20, 30)); // 60
printf("%d\n", sum_n(5, 1, 2, 3, 4, 5)); // 15
log_ints("scores", 3, 95, 88, 92);
return 0;
}四个宏的作用:
va_list args:声明一个代表"参数列表"的变量。va_start(args, 最后一个具名参数):初始化 args,从最后一个具名参数之后开始。va_arg(args, 类型):取下一个参数,必须指定类型。va_end(args):清理,配对使用。
可变参数函数没有类型检查——你传错类型编译器不会报错,运行时会得到垃圾值。这也是为什么 printf("%d", 3.14) 会输出奇怪结果——%d 期望 int,你给了 double,位模式被错误解读。
七、函数设计原则
- 单一职责:一个函数只做一件事。如果一个函数超过 50 行,考虑拆分。
- 命名清晰:动宾结构,
calc_sum比cs好 100 倍。 - 参数别太多:超过 5 个参数考虑用结构体打包。
- 避免副作用:好的函数应该是"输入 → 输出",不要偷偷改全局变量。
- 错误用返回值:C 没有 try-catch,惯例是返回 0 表示成功,非 0 表示错误码(系统调用几乎都这样)。
八、main 函数的特殊性
main 是程序入口,有两种标准形式:
int main(void):不接收命令行参数。int main(int argc, char *argv[]):接收命令行参数。argc是参数个数(包括程序名本身),argv是参数字符串数组。
main 返回 0 表示成功,非 0 表示失败。操作系统或调用脚本可以根据这个返回值判断程序是否正常退出。
小结
函数四件套:返回类型、名字、参数、函数体。原型声明让定义写在后面也能用。C 默认值传递,要修改外部变量必须传指针。递归要有基准情形。可变参数用 stdarg.h 四个宏。下一篇我们看 C 的数组——一组连续的同类型数据。
← 上一篇 C 控制流
下一篇 C 数组 →