8. 函数(一):函数的定义与调用、函数的参数传递(值传递)、函数的返回值、函数的声明与定义分离
函数,说白了就是一段可以重复使用的代码块。我刚开始学C语言时,总觉得函数是个麻烦事——明明可以直接写代码,干嘛非要绕个弯子?直到我在一个项目中写了三千多行的main函数,调试到崩溃……嗯,从那以后,我再也不敢不用函数了。
这一章我们聊聊函数的基础。别小看这些基础,我见过不少工作两三年的工程师,在函数声明和定义分离上栽跟头。咱们一步步来。
8.1 函数的定义与调用
函数的定义,就是告诉编译器:我要造一个功能模块,它叫什么、需要什么原料、会产出什么结果。调用呢,就是真正去用这个模块。
先看一个最简单的例子:
#include <stdio.h>
// 函数的定义
void sayHello() {
printf("Hello, C World!\n");
}
int main() {
// 函数的调用
sayHello();
return 0;
}
这里 sayHello 就是函数名,void 表示它没有返回值。调用时直接写函数名加括号就行。我个人习惯把函数定义放在 main 函数后面,但那样就需要提前声明——这个我们后面会讲。
核心要点:
- 函数定义 = 函数头 + 函数体
- 函数调用 = 函数名 + 实参列表
- 调用前,编译器必须“见过”这个函数(定义或声明)
8.2 函数的参数传递(值传递)
这是初学者最容易迷糊的地方。值传递,说白了就是“复制一份数据”传给函数。函数里怎么改,都不影响原来的变量。
我举个例子:
#include <stdio.h>
void swap(int a, int b) {
int temp = a;
a = b;
b = temp;
printf("函数内部:a = %d, b = %d\n", a, b);
}
int main() {
int x = 10, y = 20;
printf("调用前:x = %d, y = %d\n", x, y);
swap(x, y);
printf("调用后:x = %d, y = %d\n", x, y);
return 0;
}
运行结果:
调用前:x = 10, y = 20
函数内部:a = 10, b = 20
调用后:x = 10, y = 20
看到了吗?函数内部确实交换了,但 main 函数里的 x 和 y 纹丝不动。为什么会这样?因为传递的是 x 和 y 的副本,不是它们本身。
我曾经踩过的坑: 刚工作时写了一个排序函数,传了数组进去,在函数里交换元素,结果发现原数组没变。折腾了半天才想起来——数组名传进去其实是地址,但单个变量传进去是值拷贝。嗯,这个区别很重要。
值传递的特点:
- 形参是实参的拷贝
- 函数内修改形参不影响实参
- 适合传递基本类型数据
- 如果传递大型结构体,拷贝开销大——这时候要考虑传指针
8.3 函数的返回值
函数可以返回一个结果给调用者。用 return 语句实现。
#include <stdio.h>
int add(int a, int b) {
int sum = a + b;
return sum; // 返回计算结果
}
int main() {
int result = add(3, 5);
printf("3 + 5 = %d\n", result);
return 0;
}
这里 int add(int a, int b) 表示:这个函数返回一个 int 类型的值。return 后面跟的就是要返回的数据。
关于返回值,有几点要注意:
- 返回值类型必须和函数声明一致
- return 语句会立即结束函数执行
- void 函数可以不写 return,也可以写
return;提前结束 - 一个函数可以有多个 return,但建议保持单一出口——我个人的代码规范里,除非特殊情况,否则只用一两个 return
小技巧: 函数返回值的类型可以很灵活。比如返回指针、返回结构体,甚至返回函数指针。但初学者先掌握基本类型就好。
8.4 函数的声明与定义分离
这是实际项目中必须掌握的技能。你想想看,一个项目几十个源文件,如果每个文件都要把函数定义写一遍,那不乱套了?
函数的声明,就是告诉编译器:“嘿,有个函数长这样,你先记着,具体实现后面再说。” 定义才是真正的实现。
看一个标准做法:
头文件 myfunc.h:
#ifndef MYFUNC_H
#define MYFUNC_H
// 函数声明
int add(int a, int b);
int subtract(int a, int b);
#endif
源文件 myfunc.c:
#include "myfunc.h"
// 函数定义
int add(int a, int b) {
return a + b;
}
int subtract(int a, int b) {
return a - b;
}
主文件 main.c:
#include <stdio.h>
#include "myfunc.h"
int main() {
printf("10 + 5 = %d\n", add(10, 5));
printf("10 - 5 = %d\n", subtract(10, 5));
return 0;
}
这样做的优点很明显:
- 代码模块化,每个 .c 文件只负责一组功能
- 头文件作为接口文档,别人一看就知道这个模块提供了什么
- 编译时只编译修改过的文件,链接时再合并——大型项目编译速度的关键
重要规则:
- 声明可以多次,定义只能一次(One Definition Rule)
- 头文件里只放声明,不放定义(除非是 inline 函数或模板)
- 头文件要加 include guard(就是 #ifndef / #define / #endif 那套)
我曾经在一个项目里看到有人把函数定义写在头文件里,结果被两个 .c 文件包含,链接时报重定义错误。排查了半天才发现问题。嗯,这种坑踩过一次就记住了。
知识体系图
下面这张图帮你理清本章的核心脉络:
本章小结
函数是C语言模块化的基石。这一章我们讲了四个核心点:
- 定义与调用:先定义后调用,或者先声明后调用
- 值传递:传的是副本,函数内修改不影响原变量
- 返回值:用 return 把结果传回来,类型要匹配
- 声明与定义分离:头文件放声明,源文件放定义,这是工程化开发的基础
说实话,函数这部分内容看似简单,但很多细节在实际项目中才会暴露出来。比如值传递和指针传递的选择、返回局部变量地址的危险、头文件重复包含的问题……这些我们会在后续章节逐步深入。
你现在要做的,就是多写、多练。把每个概念都亲手敲一遍代码,看看运行结果。编程这东西,光看是学不会的。
公众号:蓝海资料掘金营,微信deep3321