C foundations / Lesson 02

让程序记住数据,
再把它说清楚。

上一课的程序每次都打印固定的文字。这一课开始使用变量:给数据起名字、说明它属于什么类型,再用正确的格式输出。学完后,你就能为“采样次数”“电压值”和“状态字符”写出清晰的 C 代码。

02 / C 语言基础建议先完成第 01 课先预测,再运行
01 / 目标

本课要亲手证明什么?

学习类型时,最有用的检验方式是让程序输出真实结果,而不是背一张字节数表。完成本课后,你应当能独立做到:

  • 声明并初始化 int、double、char 变量,用有含义的名字描述它们。
  • 解释 = 在赋值语句里的作用,预测变量重新赋值后的输出。
  • 让 printf 的格式占位符与实参类型匹配,并看懂一个格式不匹配警告。
  • 运行 sizeof 查看当前平台的类型大小,知道为什么不能把本机数字当成所有 STM32 的规则。
本课产出

一个能打印采样次数、电压和状态的 values.c;一份记录自己电脑上 sizeof(int) 的结果,以及一次“改值 → 重新编译 → 对照输出”的实验记录。

02 / 回顾

固定文字为什么不够用?

上一课的 printf("Hello, world!\n"); 无论运行多少次,都会输出同一行文字。嵌入式程序却要面对会变化的数据:今天测到多少伏、已经采样几次、当前是什么状态。我们需要在程序中给这些数据一个名字。

先看一行新代码此时先不要背语法
int sample_count = 3;

可以先把它读作:“创建一个名叫 sample_count 的整数变量,一开始放入数值 3。”这只是理解用的模型;编译器可能把值放在寄存器或优化掉某些存储,不能凭这一行推断固定的 RAM 地址。

先做一个预测:如果输出语句使用这个变量,而我们把初值从 3 改为 5,重新编译并运行后,屏幕上哪部分会变化?带着预测继续往下读。
03 / 语法

声明、初始化和名字

在 C 中,变量使用前要先声明。初学时尽量在声明的同时给出初值,这叫初始化。下面一行可以拆成四部分:

拆解变量定义类型 · 名字 · 初值 · 结束符
int sample_count = 3;
int
类型。这里表示使用一种有符号整数类型,适合本课的采样次数示例;它到底占几个字节由平台决定。
sample_count
变量名。看到名字就能猜到数据用途。C 区分大小写,所以 sample_count 与 Sample_Count 不是同一个名字。
= 3
把初值 3 放进刚声明的变量。这里的 = 表示初始化时指定值,不是数学上“左右两边永远相等”。
;
结束这条声明语句。和第一课的 printf 一样,漏写分号会得到编译诊断。

名字可以包含字母、数字和下划线,但不能以数字开头,也不能用 int、return 这样的 C 关键字。用 voltage 比用 x 更能表达用途;本课统一采用小写单词加下划线。

不要读取未初始化的局部变量。例如只写 int sample_count;,紧接着就输出它,并不能指望自动得到 0。先给值,再使用;编译器没有发出警告也不等于这种写法安全。
04 / 实验

写下并运行 values.c

在第一课的文件夹中新建 values.c,输入下面的完整程序。先预测三行输出,再编译运行。第一课的编译环境可以直接复用,不需要重新安装。

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

int main(void)
{
    int sample_count = 3;
    double voltage = 3.3;
    char grade = 'A';

    sample_count = sample_count + 1;

    printf("samples=%d\n", sample_count);
    printf("voltage=%.1f V\n", voltage);
    printf("grade=%c\n", grade);

    return 0;
}
Windows x64 · MSYS2 UCRT64

编译后运行

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

请在第一课使用的 UCRT64 终端、values.c 所在目录执行。

Ubuntu

编译后运行

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

如果提示找不到源文件,先用 pwd 和 ls 检查当前目录。

预期输出三行
samples=4
voltage=3.3 V
grade=A

第一行不是 3,因为输出前执行了 sample_count = sample_count + 1;。第二行的 %.1f 把数值显示为一位小数;它没有把变量改成字符串。第三行用 %c 显示字符。显示成 3.3 不意味着二进制浮点数能精确表示所有十进制小数。

05 / 类型

先认识四种常用的值

类型决定了数据能表达什么,以及哪些操作和输出格式适合它。本课先用足够支撑练习的四种类型;不要把电脑上的字节数或最大值硬背成 C 语言在所有板卡上的规定。

类型本课用法示例输出格式
int可正可负的整数,例如计数差值。int count = 3;%d
unsigned int从 0 开始的无符号整数;减法边界要特别留意。unsigned int ticks = 3U;%u
double带小数的近似数值,例如电压。double voltage = 3.3;%f
char一个字符值,例如状态代号。char grade = 'A';%c

'A' 使用单引号,表示字符常量;"A" 使用双引号,表示字符串,两者不能随意互换。C 里还有 float,它与 double 都用于浮点值;两者的范围与精度通常有差异,具体以当前实现为准。这一课先用 double,下一步再按目标平台认识取舍。

面向 STM32 的提醒:普通的 int 多宽、char 是否按有符号值解释,不能只凭电脑上的结果推断。在后面的硬件接口与通信课程里,我们会用 <stdint.h> 中的定宽整数类型来表达明确的数据宽度。
06 / 输出

printf 必须知道你给了什么类型

格式字符串里的占位符像一份“取值说明书”。例如 printf("samples=%d\n", sample_count); 会用后面的整数替换 %d。占位符与实参的数量、类型都要对上。

占位符本课对应数据读作
%dint按有符号十进制整数输出
%uunsigned int按无符号十进制整数输出
%f / %.1fdouble输出浮点值;后者只显示一位小数
%cchar 值调用时经过整数提升(常见平台上为 int)按字符输出
%zusizeof 返回的 size_t按无符号十进制输出大小

如果把 double voltage 交给 %d,不是“只是显示得难看”:可变参数函数无法按正确类型取值,这种格式不匹配会导致未定义行为。把编译器的格式警告当作需要修复的问题,而不是让错误程序继续运行。

显示精度 ≠ 存储精度

%.1f 只控制打印时保留一位小数。double voltage 仍按该平台的浮点格式保存;许多十进制小数无法用有限的二进制位精确表示。更深入的浮点误差留到运算课中实验。

07 / 变化

同一个变量可以被重新赋值

声明时给初值叫初始化;声明之后再写 sample_count = ...; 是赋值。C 的 = 会先计算右侧表达式,再把结果赋给左侧变量,所以这段代码是成立的:

先预测,再看答案右侧先计算
int sample_count = 3;
sample_count = sample_count + 1;
printf("%d\n", sample_count);

第二行读取旧值 3,加 1 后把 4 写回,因此输出是 4。数学等号不能这样理解;判断两个值是否相等要用另一个运算符,放到后面的条件课学习。

本课把变量写在 main 的花括号内,所以这些名字在这个函数体内可用,离开它就不能直接使用。先掌握“声明之后才能用、使用之前先有值”这两条规则,不必立刻研究全局变量。

自检:如果第二行改成加 2,会输出多少?

输出 5。先读取旧值 3,再加 2,最后把结果写回 sample_count。

08 / 平台

用 sizeof 测量,而不是猜类型大小

sizeof(类型) 给出该类型在当前 C 实现中占多少字节。结果的类型是 size_t,打印时使用 %zu。把下面三行加入 values.c 的 return 0; 前,重新编译并运行:

加到 main 内数值以本机输出为准
printf("int: %zu byte(s)\n", sizeof(int));
printf("double: %zu byte(s)\n", sizeof(double));
printf("char: %zu byte(s)\n", sizeof(char));

你会看见三个数字;本课不预填 int 和 double 的数字,因为它们依平台而定。sizeof(char) 按 C 的定义是 1 个字节,但“一个字节有几位”也由实现定义,不应把所有处理器都想成同一份内存布局。

和硬件有什么关系?将来你会把传感器数据放进寄存器或通信帧。那时必须确认字段究竟有几位,再选合适的类型;“我电脑上的 int 恰好占几个字节”不能替代协议或芯片手册。
09 / 排错

五个最容易踩到的坑

现象原因线索处理方式
出现 format '%d' expects ... 一类警告printf 格式与后面的变量类型不匹配。逐一核对 %d、%f、%c 与实参;修好后重新编译。
提示变量未声明名字拼错、大小写不同,或在声明之前使用。对照声明处的拼写和所在花括号。
提示重定义在同一作用域内又写了一次同名类型声明。如果只想改值,写 sample_count = 4;,不要重复 int。
输出还是旧数值只保存了源文件,没重新编译;或运行了另一个文件夹中的程序。按“保存 → 编译 → 运行”的顺序重做,并核对当前目录。
sizeof(int) 与别人的结果不同目标平台、编译器或 ABI 可能不同。记录本机结果,继续用程序与目标板资料核对;不要改代码去凑教程数字。
没有 warning 也要想一想

编译器警告很有用,但并不能证明程序所有路径都正确。特别是局部变量,先初始化再读取;格式占位符要与值匹配。预测和运行结果也要互相核对。

10 / 练习

做完三步,再展开答案

每一题先写下预测,然后只改一处、保存、重新编译并运行。答案用于核对思路;更重要的是你能解释程序为什么这样输出。

LEVEL 01 / 预测

采样次数变成多少?

把主例程中的 sample_count = sample_count + 1; 改成 sample_count = sample_count + 2;。在运行前写下第一行输出,并解释右侧先用的是哪个值。

查看参考答案

第一行变为 samples=5。右侧先用原来的 3,算出 5,再赋给左侧变量。若仍看到 4,检查是否重新编译。

LEVEL 02 / 格式

把电压显示成两位小数

不改变 double voltage = 3.3;,只改输出格式,让第二行显示 voltage=3.30 V。指出你改动的是变量的值,还是打印规则。

查看参考答案
printf("voltage=%.2f V\n", voltage);

改动的是格式字符串:%.2f 显示两位小数。voltage 的类型和值都没因这条语句而改变。

LEVEL 03 / 跨平台

记录你机器上的类型大小

运行本课的 sizeof 三行代码,记录本机 int、double、char 的结果。随后解释为什么不能把 int 的本机结果直接写成 STM32 通信帧的字段长度。

查看检查要点

你应当写出自己机器上的实际数字,而不是照抄一个固定答案。sizeof(char) 应为 1 字节;其他类型大小由当前实现决定。通信帧的字段宽度来自协议设计或硬件资料,代码需据此选择明确的类型。

11 / 验收

什么时候算真正学会?

  1. 不看示例也能写出一个声明并初始化的 int 变量,说明变量名、类型和初值各是什么。
  2. 主例程在你的电脑上编译无错误和未解释的警告,三行输出符合你事先的预测。
  3. 能解释 %d、%.1f、%c 和 %zu 分别对应什么值。
  4. 能用 sizeof 取得本机结果,并说出为什么目标板的数据宽度要另行核对。
下一步

变量已经能保存数据。接下来的表达式与运算课会让它们参与计算,并检查整数除法、运算顺序和类型变化。现在先把本课三个练习做完,带着自己的输出记录再前进。

资料

对照官方资料继续查

类型的实际大小、浮点实现和工具链警告随平台变化。操作具体板卡前,以所用编译器、芯片手册及通信协议为准。