一、函数初探:为什么需要函数?
说实话,我刚开始学C语言那会儿,对函数这东西挺不以为然的。
当时写个流水灯程序,把所有代码全塞在main()里,几百行下来,自己看着都头疼。后来有个师兄看了我的代码,说了句让我记到现在的话——「你这是在写散文,不是写程序」。
嗯,函数这东西,说白了就是让你从「散文」变成「结构化写作」的关键工具。
1.1 没有函数的日子
你想想看,如果没有函数,你的代码会是什么样?
// 没有函数的写法
#include <stdio.h>
int main() {
int a = 5, b = 3;
int sum1 = a + b;
printf("5 + 3 = %d\n", sum1);
int c = 10, d = 20;
int sum2 = c + d;
printf("10 + 20 = %d\n", sum2);
int e = 100, f = 200;
int sum3 = e + f;
printf("100 + 200 = %d\n", sum3);
return 0;
}
这段代码有什么问题?
- 重复代码:加法逻辑写了三遍
- 难以维护:如果想改成乘法,得改三个地方
- 可读性差:一眼看去全是细节,抓不住重点
我在一个嵌入式项目中见过更夸张的——有人把I2C读写逻辑复制粘贴了二十多次。后来要改时序参数,改到崩溃。嗯,这就是没有函数的代价。
1.2 函数来了
用函数改写一下,世界就清爽了:
// 有函数的写法
#include <stdio.h>
// 函数的定义
int add(int x, int y) {
return x + y;
}
int main() {
printf("5 + 3 = %d\n", add(5, 3));
printf("10 + 20 = %d\n", add(10, 20));
printf("100 + 200 = %d\n", add(100, 200));
return 0;
}
看到了吗?核心逻辑被提取到add()函数里,main()只负责调用。这就是函数的本质——一段可重用的代码块。
核心理解:函数就是把一段逻辑封装起来,给它起个名字,以后想用就喊这个名字。就像你工具箱里的螺丝刀,用的时候拿出来,不用就放回去。
二、函数的定义与声明
这里有个坑,我当年踩过。函数的定义和声明,其实是两回事。
2.1 函数定义
定义就是「把函数写出来」,包含完整的实现:
// 函数定义:返回类型 函数名(参数列表) { 函数体 }
int max(int a, int b) {
return (a > b) ? a : b;
}
定义包含四个要素:
| 要素 | 说明 | 示例 |
|---|---|---|
| 返回类型 | 函数返回什么类型的数据 | int, void, float... |
| 函数名 | 标识符,用来调用函数 | max, add, init_uart |
| 参数列表 | 函数接收的输入 | (int a, int b) |
| 函数体 | 花括号里的具体逻辑 | { return ... } |
2.2 函数声明
声明就是「告诉编译器有这么一个函数存在」,但不给出实现:
// 函数声明(也叫函数原型)
int max(int a, int b);
// 或者可以省略参数名(我个人习惯保留,可读性更好)
int max(int, int);
为什么要声明?因为C语言是顺序编译的。如果你把main()写在前面,而max()定义在后面,编译器看到max()调用时会一脸懵——「这谁啊?」
我的习惯:头文件里放声明,源文件里放定义。这样别的文件想用你的函数,只要#include头文件就行。我在一个STM32项目里就是这么组织的,后来代码量到两万行,结构依然清晰。
2.3 定义 vs 声明:一张图说清楚
// 声明:只有签名,没有身体
int max(int a, int b); // 声明
// 定义:签名 + 身体
int max(int a, int b) { // 定义开始
return (a > b) ? a : b;
} // 定义结束
注意:一个函数只能定义一次,但可以声明多次。如果你在多个源文件里都定义了同一个函数,链接器会报重复定义错误。我曾经在一个团队项目里遇到过——两个人各自写了一个同名函数,链接时炸了,排查了半天。
三、函数的本质:一段可重用的代码块
咱们往底层想想。函数到底是什么?
在内存里,函数就是一段连续的指令序列。当你调用函数时,CPU会跳转到这段指令的起始地址,执行完再跳回来。
我画了一张图,帮你理解这个过程:
从这张图你能看到:
- main() 执行到调用指令时,CPU跳转到add()的地址
- add() 执行完自己的指令,再跳回main()的下一条指令
- 关键点:add()的代码在内存里只有一份,但可以被调用无数次
函数的本质:一段有名字、可重用的指令序列。它把「做什么」和「怎么做」分开了——调用者只关心「做什么」,函数内部关心「怎么做」。
四、为什么说函数是模块化的基石?
我做过一个车载诊断系统的项目,代码量大概五万行。如果没有函数,那简直不敢想象。
函数带来的好处,我总结了几点:
- 复用性:写一次,用多次。比如CRC校验函数,整个项目到处都在用。
- 可读性:函数名就是注释。看到
calculate_checksum(),你就知道它在算什么。 - 可测试性:可以单独测试每个函数。我习惯写完一个函数就写个测试用例跑一下。
- 分工协作:你写你的函数,我写我的函数,最后拼起来就行。
- 抽象与封装:调用者不需要知道内部实现。就像你开车,不需要知道发动机怎么工作的。
一个实用建议:写函数时,尽量让一个函数只做一件事。如果一个函数超过50行,你就该想想能不能拆分了。我见过有人一个函数写了800行,那已经不是函数了,那是灾难。
五、避坑指南
最后分享几个我踩过的坑:
坑1:忘记声明直接调用
我曾经在一个.c文件里调用了另一个.c文件的函数,但忘了加声明。编译器给了个警告,我没在意。结果运行时参数传递错了,排查了两天。后来养成习惯——所有跨文件调用的函数,都在头文件里声明。
坑2:函数名冲突
在一个大项目里,两个模块都定义了一个叫init()的函数。链接时直接报错。我的建议是:给函数名加前缀,比如uart_init()、timer_init(),这样既清晰又避免冲突。
坑3:返回局部变量的地址
这个坑比较深,后面章节会细讲。简单说就是:不要返回局部变量的指针,因为函数返回后局部变量就销毁了。
好了,关于函数的基础就聊到这儿。记住一句话:函数是C语言的模块化单元,是你写出可维护代码的第一步。