一、函数:C语言的模块化基石
说实话,我刚开始学C语言那会儿,觉得函数就是个「黑盒子」——把代码塞进去,出来个结果。后来做了几个嵌入式项目才明白,函数这东西,用好了能让代码像搭积木一样清晰,用不好就是一团乱麻。
今天咱们聊聊函数的基础。别小看这些基础,我在项目里见过太多bug,都是因为函数声明和定义没搞对,或者参数传递理解有偏差。嗯,咱们一个一个说。
1.1 函数的定义与声明
先搞清楚两个概念:定义和声明。很多人混着用,其实差别很大。
- 函数定义:写出函数的完整实现,包括函数体。编译器看到定义,就知道这个函数具体干什么。
- 函数声明:也叫函数原型,只告诉编译器「有这个函数,参数长这样,返回值长那样」。具体实现?后面再说。
举个例子你就明白了:
// 函数声明(原型)
int add(int a, int b);
// 函数定义
int add(int a, int b) {
return a + b;
}
我个人习惯,在头文件里放声明,在源文件里放定义。这样结构清晰,别人看你的代码也舒服。
重要原则:调用函数之前,编译器必须见过它的声明或定义。否则编译器会默认返回int,这很容易出问题。
1.2 函数原型——你的代码说明书
函数原型长这样:
返回类型 函数名(参数类型1 参数名1, 参数类型2 参数名2, ...);
比如:
float calculate_average(int scores[], int count);
为什么要有函数原型?我在项目中遇到过一件事:有个同事写了个函数,返回float,但调用前没声明。编译器默认当int处理,结果数据全错了。查了一下午,最后发现是原型缺失。你说冤不冤?
我的建议:每个源文件对应的头文件里,把所有对外公开的函数都声明一遍。内部函数用static修饰,加个注释说明用途。
1.3 参数传递——值传递的本质
C语言里,参数传递只有一种方式:值传递。说白了,就是把实参的值复制一份给形参。函数里改形参,不影响实参。
看代码:
void swap(int x, int y) {
int temp = x;
x = y;
y = temp;
// 这里x和y交换了,但外面的a和b纹丝不动
}
int main() {
int a = 5, b = 10;
swap(a, b);
printf("a=%d, b=%d\n", a, b); // 输出:a=5, b=10
return 0;
}
为什么会这样?因为swap函数拿到的是a和b的副本。你想想看,你给朋友一张照片,朋友在照片上画胡子,你本人脸上会多胡子吗?不会。
那怎么实现真正的交换?用指针:
void swap_real(int *x, int *y) {
int temp = *x;
*x = *y;
*y = temp;
}
int main() {
int a = 5, b = 10;
swap_real(&a, &b);
printf("a=%d, b=%d\n", a, b); // 输出:a=10, b=5
return 0;
}
这里传递的是地址的副本,但通过地址可以修改原变量。嗯,这个区别很重要,面试常考。
避坑指南:我曾经见过有人把结构体直接传值,结构体很大(比如几百字节),每次调用都复制一遍,性能直接崩了。大结构体请用指针传递。
1.4 返回值与return语句
return有两个作用:
- 返回一个值给调用者
- 结束当前函数的执行
看个例子:
int max(int a, int b) {
if (a > b) {
return a; // 这里就结束了,后面的代码不执行
}
return b;
}
注意几点:
- 返回值类型要和函数声明一致。声明int,就别return 3.14。
- void函数可以没有return,或者写return;(不带值)。
- 函数执行到return,立刻返回,后面的代码不执行。
一个常见的坑:返回局部变量的指针。局部变量在函数结束后就销毁了,返回它的地址,外面拿到的是野指针。我曾经在项目里排查过一个随机崩溃的bug,最后发现就是有人返回了局部数组的地址。
1.5 知识体系总览
下面这张图,把函数的基础知识串起来了。你看一遍,心里就有谱了。
1.6 实战要点总结
| 知识点 | 要点 | 常见错误 |
|---|---|---|
| 函数声明 | 调用前必须声明,放在头文件 | 忘记声明,编译器默认int返回 |
| 函数定义 | 只能定义一次,可以声明多次 | 重复定义导致链接错误 |
| 值传递 | 形参是实参的副本 | 以为函数内修改会影响外面 |
| return | 返回值类型要匹配 | 返回局部变量指针 |
一个小技巧:写函数时,先写声明(原型),再写定义。这样你脑子里先想清楚「这个函数输入什么、输出什么」,写起来思路更清晰。我这些年一直这么干,很少因为函数接口问题返工。
好了,函数的基础就这些。说白了就是三件事:告诉编译器有这个函数(声明),写出它的具体逻辑(定义),搞清楚数据怎么进去怎么出来(参数和返回值)。把这些搞明白,后面的函数进阶内容就好学了。
公众号:蓝海资料掘金营,微信deep3321