导学案6 · 函数:把重复的逻辑打包¶
前置要求:导学案5(会数组、会循环、会 switch) 参考课程:浙江大学 翁恺《C 语言程序设计》第 7 章(函数) 预计用时:1.5 小时(按需自取) 本节目标:说清函数是什么、有哪几个部分;会定义、声明、调用函数;理解参数是"值传递"
6.0 序¶
你已经用过很多函数了:printf、scanf、main——它们都是函数。这一节你要学会自己写函数。
为什么需要函数?三句话:
- 重复的代码只写一遍。 同一段逻辑要用五次,就写成一个函数调用五次,改 bug 只改一处。
- 把大问题拆成小块。 一个 500 行的
main没人看得懂;拆成十个各司其职的函数,每个都能单独看懂、单独测试。 - 别人的成果直接拿来用。 你调
printf时从没看过它的源码。以后 STM32 的 HAL 库、实验室的飞控代码,全是"别人写好的函数",你要做的就是调用。
本节路线:函数是什么 → 四个组成部分 → 定义、调用与声明 → 参数怎么传 → 库函数 → 练习
本节不讲:函数指针、递归、多文件工程(
.h/.c分离)。另外 C 语言没有"重载"这个词对应的东西,别处看到不用管。
6.1 函数是什么¶
把函数想成一台加工机器:你往里投料(参数),它干活,然后吐出结果(返回值)。
第一个自己写的函数:
#include <stdio.h>
void sayHello(void) /* 自定义函数:打招呼 */
{
printf("你好,飞行器创新实验室\n");
}
int main(void)
{
sayHello(); /* 调用:想调几次调几次 */
sayHello();
return 0;
}
输出:
注意两件事:
- 定义函数不会执行它,那只是"登记了一台机器";调用(写
sayHello();)才会真的跑。 main也是一个函数,是程序的大门。你写的其他函数,都是从main里被叫起来的。
6.2 函数的四个组成部分¶
拿一个真函数对号入座:
int max(int a, int b) /* 返回值类型 int,函数名 max,参数列表 (int a, int b) */
{
if (a > b) {
return a;
}
return b; /* 返回值 */
}
| 部分 | 例子 | 说明 |
|---|---|---|
| 返回值类型 | int |
这台机器吐出什么类型的结果;不需要吐东西就写 void |
| 函数名 | max |
命名规则和变量完全一样:字母数字下划线、不能数字开头、区分大小写 |
| 参数列表 | (int a, int b) |
投什么料;不需要投料就写 (void) |
| 函数体 + return | { ... return b; } |
怎么干活、吐什么;return 同时会立刻结束这个函数 |
两个必须记住的点:
void有两种用法:写在返回值位置表示"不返回任何东西";写在参数位置表示"不接受任何参数"。- 非
void函数必须每条路都有return。 如果某条路走到函数末尾还没 return,返回值就是垃圾,而且编译器常常只给警告、不给错误。
#include <stdio.h>
int max(int a, int b)
{
if (a > b) {
return a;
}
return b;
}
int main(void)
{
int r = max(3, 7); /* 返回值可以存起来 */
printf("max(3, 7) = %d\n", r);
printf("max(10, max(4, 6)) = %d\n", max(10, max(4, 6))); /* 也可以套着用 */
return 0;
}
输出:
6.3 定义、调用与声明¶
函数要先"登记"才能被调用。登记有两种方式:定义(连机器一起造出来)和声明(只贴一张"有这么台机器"的说明书)。
最省事的写法:把函数定义写在 main 前面,本导学案全部采用这种写法。
但有时你希望 main 在最上面(读代码时先看主干),函数放在后面。这时编译器从上往下读,读到 main 里的 max(3, 7) 时还不认识 max,会报错。解决办法是先声明:
#include <stdio.h>
int max(int a, int b); /* 声明(函数原型):只有签名,没有函数体,以分号结尾 */
int main(void)
{
printf("max(3, 7) = %d\n", max(3, 7));
return 0;
}
int max(int a, int b) /* 定义:真正的机器在这里 */
{
if (a > b) {
return a;
}
return b;
}
输出:
声明和定义的差别只有一处:声明没有函数体,结尾是分号。 声明里的参数名甚至可以省略(int max(int, int);),但写上更清楚。
一句话预告(不展开):以后写多文件的 STM32 工程,声明放在
.h文件里,定义放在.c文件里。你在 HAL 库里看到的那满屏.h,干的就是这件事。
6.4 参数是怎么传的:值传递(本节最重要的一节)¶
先看一个实验:
#include <stdio.h>
void swap(int a, int b) /* a、b 叫形参 */
{
int t = a;
a = b;
b = t;
printf("函数内:a = %d, b = %d\n", a, b);
}
int main(void)
{
int x = 1, y = 2; /* x、y 叫实参 */
swap(x, y);
printf("函数外:x = %d, y = %d\n", x, y);
return 0;
}
输出:
函数里明明换好了,外面的 x 和 y 却纹丝不动。为什么?
C 语言的参数传递是"值传递":调用时把实参的值复制一份交给形参,函数里操作的是副本。 函数一结束,副本销毁,原件毫发无损。
所以 swap(x, y) 里被交换的,是 x、y 的两份复印件。这个结论非常重要,请记牢。
顺带两个推论:
- 形参名和实参名可以不同(上面就是
a/b对x/y),它们是互相独立的变量。 - 函数里声明的变量叫局部变量,只在这个函数活着期间存在;两个函数里的同名变量互不相干,
main里的也一样。
那如果我真的想让函数改到外面的变量呢?——把地址传进去。这就是下一节(导学案7 · 指针)要解决的事,也是 scanf 为什么要写 & 的原因。这里先留个悬念。
6.5 库函数:调用别人写好的函数¶
#include <stdio.h> 的本质,是把别人写好的函数的"说明书"拿进来,然后你就能调用了。常用的几个头文件:
| 头文件 | 里面有什么 | 例子 |
|---|---|---|
<stdio.h> |
输入输出 | printf、scanf、getchar |
<stdlib.h> |
杂项:随机数、取绝对值、内存 | rand、srand、abs |
<math.h> |
数学 | sqrt、fabs、pow |
<string.h> |
字符串 | strlen、strcpy、strcmp |
6.5.1 随机数¶
#include <stdio.h>
#include <stdlib.h> /* rand / srand */
#include <time.h> /* time */
int main(void)
{
srand((unsigned)time(NULL)); /* 用当前时间做种子,每次运行结果才不一样 */
for (int i = 0; i < 5; i++) {
printf("%d ", rand() % 101); /* 0 ~ 100 的随机整数 */
}
printf("\n");
printf("学号范围示例:%d\n", rand() % 11 + 100); /* 100 ~ 110 */
return 0;
}
输出(每次运行都不同,下面是其中一次的结果):
两条要点:
rand()返回一个很大的随机整数,取余是控制范围的通用办法:rand() % 101得 0~100,rand() % 11 + 100得 100~110。- 以上随机数本质是伪随机,它通过一个种子通过特定规律生成一串数字,如果种子一样那么生成的数字也就一样,写
srand(...)的目的就是让时间当种子,实现每次调用时种子变化,产生的数字也就变成了随机数。
6.5.2 数学函数¶
#include <stdio.h>
#include <math.h>
int main(void)
{
printf("sqrt(2) = %.4f\n", sqrt(2.0));
printf("fabs(-3.5) = %.1f\n", fabs(-3.5));
printf("2 的 10 次方 = %.0f\n", pow(2.0, 10.0));
return 0;
}
输出:
<math.h> 里的函数参数和返回值都是 double,所以写 sqrt(2.0) 比写 sqrt(2) 更规范(写 2 也能跑,会被自动转成 double)。
6.6 综合练习¶
本节练习用于自练与自查,不强制完成、不需要提交;需要提交的是实验室统一布置的作业(作业一 / 作业二)。 参考代码统一放在本节末尾,做完之前不要翻。
练习 1|偶数判断¶
写一个函数 int isEven(int n),是偶数返回 1,否则返回 0。主函数读入一个整数,输出"是偶数"或"不是偶数"。
练习 2|三数取最大¶
写一个函数 int max3(int a, int b, int c),返回三个数里的最大值。要求函数体内调用两次两数取大的逻辑(先写一个 max2)。
练习 3|温度转换函数¶
把导学案2的摄氏转华氏改写成函数 double c2f(double c),主函数读入摄氏温度,输出华氏温度(保留一位小数)。
练习 4|质数与选数¶
写一个函数 int isPrime(int n) 判断质数(n >= 2)。然后读入 4 个正整数,统计它们两两相加得到的 6 个和里有几个质数。(这就是作业二第 2 题)
练习 5(选做)|用函数写冒泡排序¶
用函数实现导学案5的冒泡排序:void sortDesc(int a[], int len),把数组从大到小排好。提示:数组作参数的写法导学案7会正式讲,但你现在就可以先试 int a[] 这种写法。
6.7 本节小结与自查¶
学完本节,你应该能做到:
- 说清函数的四个组成部分,知道
void在返回值位置和参数位置分别是什么意思 - 自己定义并调用一个函数,知道"定义不等于执行"
- 知道声明(函数原型)长什么样、什么时候必须写
- 说清"值传递":为什么函数里改形参,改不到外面的实参
- 知道局部变量的作用范围
- 会用
rand()/srand()产生指定范围的随机数
下一节:导学案7 · 指针。6.4 留下的悬念——怎么让函数改到外面的变量——在那里解开。
6.8 练习参考代码(做完再看)¶
练习 1 参考代码¶
#include <stdio.h>
int isEven(int n)
{
if (n % 2 == 0) {
return 1;
}
return 0;
}
int main(void)
{
int n = 0;
printf("请输入一个整数:");
scanf("%d", &n);
if (isEven(n)) {
printf("%d 是偶数\n", n);
} else {
printf("%d 不是偶数\n", n);
}
return 0;
}
练习 2 参考代码¶
#include <stdio.h>
int max2(int a, int b)
{
if (a > b) {
return a;
}
return b;
}
int max3(int a, int b, int c)
{
return max2(max2(a, b), c);
}
int main(void)
{
printf("max3(3, 9, 5) = %d\n", max3(3, 9, 5));
return 0;
}
输出:max3(3, 9, 5) = 9
练习 3 参考代码¶
#include <stdio.h>
double c2f(double c)
{
return c * 9.0 / 5.0 + 32.0;
}
int main(void)
{
double c = 0.0;
printf("请输入摄氏温度:");
scanf("%lf", &c);
printf("%.1f 摄氏度 = %.1f 华氏度\n", c, c2f(c));
return 0;
}
练习 4 参考代码¶
#include <stdio.h>
int isPrime(int n)
{
if (n < 2) {
return 0;
}
for (int i = 2; i * i <= n; i++) {
if (n % i == 0) {
return 0;
}
}
return 1;
}
int main(void)
{
int a[4] = {0};
int count = 0;
printf("请输入 4 个正整数:");
for (int i = 0; i < 4; i++) {
scanf("%d", &a[i]);
}
for (int i = 0; i < 4; i++) {
for (int j = i + 1; j < 4; j++) {
int s = a[i] + a[j];
if (isPrime(s)) {
count++;
}
}
}
printf("质数的个数 = %d\n", count);
return 0;
}
输入 1 2 3 4,输出 质数的个数 = 4,和作业二给的示例一致。
函数是"把一段逻辑起个名字"。从这一节起,你写的不再是"一长串步骤",而是"一堆各司其职的小机器"。
这个习惯会直接决定你以后能不能读懂飞控代码:那里几乎每一行都在调用别人写好的函数。
作者:Mike-lin300
最后更新:2026-09-25
更新说明:无(新编写)