C foundations / Lesson 06

给一段任务起名字,
让函数完成它。

把采样值加上偏移量,再输出结果:我们可以把两个职责拆成两个函数。本课跟随一次真实调用,理解声明、参数、返回值,以及变量在哪个范围可见。

06 / C 语言基础建议先完成循环与分支先追踪调用,再运行
01 / 目标

把程序拆成可以解释的小任务

函数把一段处理组织成有名字、可调用的单元。完成本课后,你应当能够:

  • 识别函数声明、定义和调用各自出现的位置。
  • 说明参数怎样传入,以及 return 怎样把结果交给调用者。
  • 用整数例子解释按值传递和局部变量的作用范围。
  • 使用带返回值的函数与不返回值的 void 函数。
本课产出

一个由 main、计算函数和输出函数组成的小程序。它只计算模拟采样值,没有读取真实传感器。

02 / 实验

先写一个完整程序

创建 lesson06.c。先猜每个输出中的数字,再编译运行:

lesson06.cC11 · 本课主例程
#include <stdio.h>

int add_offset(int value, int offset);
void print_sample(int sample);

int main(void)
{
    int sample = 12;
    int adjusted = add_offset(sample, 3);

    print_sample(adjusted);
    printf("original=%d\n", sample);
    return 0;
}

int add_offset(int value, int offset)
{
    int result = value + offset;
    return result;
}

void print_sample(int sample)
{
    printf("sample=%d\n", sample);
}
WINDOWS · MSYS2 UCRT64

编译与运行

gcc -std=c11 -Wall -Wextra lesson06.c -o lesson06.exe
./lesson06.exe
UBUNTU

编译与运行

gcc -std=c11 -Wall -Wextra lesson06.c -o lesson06
./lesson06

预期输出:

sample=15
original=12

如果源代码改动后仍得到旧输出,请确认已保存、重新编译,并运行当前目录里的可执行文件。

03 / 调用过程

程序在函数之间怎样走?

  1. 01
    进入 main

    sample 初始化为 12,接着计算 add_offset(sample, 3)。

  2. 02
    进入 add_offset

    形参 value 得到 12,形参 offset 得到 3。计算 result = 15。

  3. 03
    返回 main

    return result; 返回 15;调用表达式的结果用于初始化 adjusted。

  4. 04
    调用 print_sample

    它收到 15 并打印 sample=15;然后控制流程返回 main。

  5. 05
    继续执行 main

    打印 original=12,最后 return 0; 结束本次 main 调用。

理解变量同名:main 里的 sample 与 print_sample 的形参 sample 是不同函数中的不同变量,虽然名字相同。
04 / 定义

把函数定义拆成四部分

int add_offset(int value, int offset)
{
    int result = value + offset;
    return result;
}
int
返回类型。这个函数向调用者返回一个 int 结果。
add_offset
函数名;名字应说明它承担的任务。
int value, int offset
形参列表:每个参数都分别写类型。调用时提供对应数量和类型兼容的实参。
花括号内的函数体
执行声明和计算;return result; 返回结果并结束当前函数调用。

返回值和打印是不同的事。add_offset 把结果交给调用者,不负责显示;print_sample 负责输出。把职责分开,后续换成串口或其他输出方式时更容易定位修改位置。

示例中的整数范围很小。对于更大输入,value + offset 还要先确认结果可由 int 表示;有符号整数溢出不能作为可靠计算结果。

05 / 声明

调用之前,让编译器知道函数长什么样

主例程在 main 上方写了两条函数原型:

int add_offset(int value, int offset);
void print_sample(int sample);

原型告知函数的名字、返回类型和参数类型,结尾有分号;下面带花括号和函数体的部分是定义。函数定义位于调用点之前时,其定义本身也提供声明;本课为了看清分工,先写原型,再把定义放在 main 后面。

声明与定义必须保持类型兼容,调用时也应核对参数数量与类型。int add_offset(int, int); 在原型中省略参数名也可以,但为便于阅读,本课保留有意义的名字。

void 参数列表:在 C11 中,写 int read_value(void); 表示无参数;不要把旧式的 int read_value(); 当作等价的参数检查方式。
06 / 参数

整数参数按值传递

调用 add_offset(sample, 3) 时,sample 和 3 是实参;函数定义中的 value 和 offset 是形参。C 将实参的值用于初始化形参变量。以整数为例,即使函数内部随后修改自己的形参,也不会直接改变调用者的整数变量。

void change_copy(int value)
{
    value = 99;  /* 修改的是形参变量 */
}

/* 若在 main 中: */
int number = 12;
change_copy(number);
printf("%d\n", number);  /* 仍为 12 */

如果希望得到更新后的整数,可以让函数返回新值并由调用者赋值,例如 number = add_offset(number, 3);。以后学习指针时会看到另一种情况:传递的是地址的值,函数可在有效条件下通过它访问被指向的对象;并不是把参数变成了按引用传递。

不要依赖多个实参表达式按从左到右的顺序求值。把需要明确先后顺序、会改变同一变量的步骤分成独立语句,更容易检查。

07 / VOID

不返回数值的函数

void print_sample(int sample)
{
    printf("sample=%d\n", sample);
}

void 返回类型表示调用者不能从这个函数取得一个返回值。它可以执行打印等动作,也可以写 return; 提前结束;到达函数体末尾同样结束调用。本例直接写 print_sample(adjusted);,没有把它当成整数赋值。

void 作为返回类型与参数列表里的 (void) 含义不同:前者表示没有返回值,后者表示该函数不接受参数。

08 / 作用域

变量在哪儿可以使用?

result 定义在 add_offset 的函数体内,因此它的名字在该函数体的相应作用域内使用;main 不能直接写 printf("%d", result); 读取它。要把计算结果带回 main,本例使用 return。

main 的 sample 和 adjusted 也属于各自声明位置所在的块作用域。两个不同函数可以有同名局部变量,它们不会因为同名而成为同一个对象。更深入的存储期和链接在多文件课程中继续讨论。

嵌入式联系

将数据转换、限幅计算和输出拆成职责清楚的函数,有助于先在电脑上验证算法,再根据具体板卡接入外设;真实外设驱动还需要检查芯片与工具链的要求。

09 / 排错

函数调用时常见的四个问题

现象检查点处理
编译器提示函数未声明调用点之前是否有正确声明,名称拼写是否一致补充匹配的原型,或将函数定义放在调用之前。
参数数量或类型不匹配原型、定义和调用三处是否对应对照每个参数的类型与顺序,重新编译。
期待原变量被改写函数是否只修改了整数形参返回新值并在调用处赋值,或以后学习指针。
有返回类型的函数没有返回结果每条应返回结果的执行路径是否有匹配的 return补齐返回路径,认真处理编译器警告。

在非 void 函数中,不应依赖到达函数末尾后使用未返回的结果;确保设计的每条路径都返回有效值。不要忽略编译器诊断。

10 / 练习

用三道小题检查调用过程

LEVEL 01 / 预测

修改偏移量

把主程序的 add_offset(sample, 3) 改成 add_offset(sample, 5)。预测两行输出。

查看参考答案

第一行是 sample=17,第二行仍是 original=12。原变量未被该函数直接修改。

LEVEL 02 / 返回值

更新原变量

如果希望 sample 自己也得到增加 3 后的值,可以在调用处怎样写?

查看参考答案
sample = add_offset(sample, 3);

函数返回新值后,由调用者将其赋给 sample;计算仍须保证结果在类型范围内。

LEVEL 03 / 原型

让声明与定义对应

写出一个名为 twice 的函数原型:接受一个 int,返回一个 int。

查看参考答案
int twice(int value);

这是声明,末尾分号不能漏。实现翻倍前需要考虑有符号整数的可表示范围。

11 / 验收

能说清函数的输入、动作和输出

  1. 能从主例程找出原型、定义和两次调用。
  2. 能解释 value、offset 分别收到的值和 return 的作用。
  3. 能解释为什么打印的原始变量仍为 12。
  4. 能编译运行、改一个参数并核对新的输出。

接下来学习数组与字符串:让函数和循环处理多组数据,同时注意下标边界。

资料

对照官方资料继续查