编译与运行
gcc -std=c11 -Wall -Wextra lesson06.c -o lesson06.exe
./lesson06.exeC foundations / Lesson 06
把采样值加上偏移量,再输出结果:我们可以把两个职责拆成两个函数。本课跟随一次真实调用,理解声明、参数、返回值,以及变量在哪个范围可见。
函数把一段处理组织成有名字、可调用的单元。完成本课后,你应当能够:
return 怎样把结果交给调用者。void 函数。一个由 main、计算函数和输出函数组成的小程序。它只计算模拟采样值,没有读取真实传感器。
创建 lesson06.c。先猜每个输出中的数字,再编译运行:
#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);
}gcc -std=c11 -Wall -Wextra lesson06.c -o lesson06.exe
./lesson06.exegcc -std=c11 -Wall -Wextra lesson06.c -o lesson06
./lesson06预期输出:
sample=15
original=12如果源代码改动后仍得到旧输出,请确认已保存、重新编译,并运行当前目录里的可执行文件。
sample 初始化为 12,接着计算 add_offset(sample, 3)。
形参 value 得到 12,形参 offset 得到 3。计算 result = 15。
return result; 返回 15;调用表达式的结果用于初始化 adjusted。
它收到 15 并打印 sample=15;然后控制流程返回 main。
打印 original=12,最后 return 0; 结束本次 main 调用。
main 里的 sample 与 print_sample 的形参 sample 是不同函数中的不同变量,虽然名字相同。int add_offset(int value, int offset)
{
int result = value + offset;
return result;
}intint 结果。add_offsetint value, int offsetreturn result; 返回结果并结束当前函数调用。返回值和打印是不同的事。add_offset 把结果交给调用者,不负责显示;print_sample 负责输出。把职责分开,后续换成串口或其他输出方式时更容易定位修改位置。
示例中的整数范围很小。对于更大输入,value + offset 还要先确认结果可由 int 表示;有符号整数溢出不能作为可靠计算结果。
主例程在 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(); 当作等价的参数检查方式。调用 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);。以后学习指针时会看到另一种情况:传递的是地址的值,函数可在有效条件下通过它访问被指向的对象;并不是把参数变成了按引用传递。
不要依赖多个实参表达式按从左到右的顺序求值。把需要明确先后顺序、会改变同一变量的步骤分成独立语句,更容易检查。
void print_sample(int sample)
{
printf("sample=%d\n", sample);
}void 返回类型表示调用者不能从这个函数取得一个返回值。它可以执行打印等动作,也可以写 return; 提前结束;到达函数体末尾同样结束调用。本例直接写 print_sample(adjusted);,没有把它当成整数赋值。
void 作为返回类型与参数列表里的 (void) 含义不同:前者表示没有返回值,后者表示该函数不接受参数。
result 定义在 add_offset 的函数体内,因此它的名字在该函数体的相应作用域内使用;main 不能直接写 printf("%d", result); 读取它。要把计算结果带回 main,本例使用 return。
main 的 sample 和 adjusted 也属于各自声明位置所在的块作用域。两个不同函数可以有同名局部变量,它们不会因为同名而成为同一个对象。更深入的存储期和链接在多文件课程中继续讨论。
将数据转换、限幅计算和输出拆成职责清楚的函数,有助于先在电脑上验证算法,再根据具体板卡接入外设;真实外设驱动还需要检查芯片与工具链的要求。
| 现象 | 检查点 | 处理 |
|---|---|---|
| 编译器提示函数未声明 | 调用点之前是否有正确声明,名称拼写是否一致 | 补充匹配的原型,或将函数定义放在调用之前。 |
| 参数数量或类型不匹配 | 原型、定义和调用三处是否对应 | 对照每个参数的类型与顺序,重新编译。 |
| 期待原变量被改写 | 函数是否只修改了整数形参 | 返回新值并在调用处赋值,或以后学习指针。 |
| 有返回类型的函数没有返回结果 | 每条应返回结果的执行路径是否有匹配的 return | 补齐返回路径,认真处理编译器警告。 |
在非 void 函数中,不应依赖到达函数末尾后使用未返回的结果;确保设计的每条路径都返回有效值。不要忽略编译器诊断。
把主程序的 add_offset(sample, 3) 改成 add_offset(sample, 5)。预测两行输出。
第一行是 sample=17,第二行仍是 original=12。原变量未被该函数直接修改。
如果希望 sample 自己也得到增加 3 后的值,可以在调用处怎样写?
sample = add_offset(sample, 3);函数返回新值后,由调用者将其赋给 sample;计算仍须保证结果在类型范围内。
写出一个名为 twice 的函数原型:接受一个 int,返回一个 int。
int twice(int value);这是声明,末尾分号不能漏。实现翻倍前需要考虑有符号整数的可表示范围。
value、offset 分别收到的值和 return 的作用。接下来学习数组与字符串:让函数和循环处理多组数据,同时注意下标边界。