编译并运行
gcc -std=c11 -Wall -Wextra lesson10.c -o lesson10.exe
./lesson10.exeC foundations / Lesson 10
传感器的读数、阈值和状态本来属于同一个对象。用结构体把它们组织起来,再用枚举表达状态,并学习对象、指针和函数之间如何配合。
单独的 raw、threshold 和 state 可以表示一个传感器,但当传感器变多时,变量容易混淆。结构体让一组有关联的成员属于同一个对象;枚举给状态值起可读的名字。
. 和 ->,理解指针有效性与 const 的作用。一个创建模拟传感器、判断阈值并打印状态的 lesson10.c。数值是示例,程序没有读取真实硬件。
先预测 raw=42、阈值为 40 时的状态,再保存以下内容为 lesson10.c:
#include <stddef.h>
#include <stdio.h>
enum SensorState {
SENSOR_READY = 0,
SENSOR_HIGH = 1,
SENSOR_ERROR = 2
};
typedef struct Sensor {
int raw;
int threshold;
enum SensorState state;
} Sensor;
Sensor sensor_make(int raw, int threshold);
enum SensorState sensor_classify(const Sensor *sensor);
int main(void)
{
Sensor sensor = sensor_make(42, 40);
sensor.state = sensor_classify(&sensor);
printf("raw=%d threshold=%d\n", sensor.raw, sensor.threshold);
switch (sensor.state) {
case SENSOR_READY:
puts("state=READY");
break;
case SENSOR_HIGH:
puts("state=HIGH");
break;
case SENSOR_ERROR:
puts("state=ERROR");
break;
default:
puts("state=UNKNOWN");
break;
}
printf("code=%d\n", (int)sensor.state);
return 0;
}
Sensor sensor_make(int raw, int threshold)
{
Sensor result = { .raw = raw, .threshold = threshold, .state = SENSOR_READY };
return result;
}
enum SensorState sensor_classify(const Sensor *sensor)
{
if (sensor == NULL) {
return SENSOR_ERROR;
}
if (sensor->raw >= sensor->threshold) {
return SENSOR_HIGH;
}
return SENSOR_READY;
}gcc -std=c11 -Wall -Wextra lesson10.c -o lesson10.exe
./lesson10.exegcc -std=c11 -Wall -Wextra lesson10.c -o lesson10
./lesson10预期输出:
raw=42 threshold=40
state=HIGH
code=1程序先生成一个结构体对象,再通过它的地址判断状态。输出部分用 switch 区分三种已列出的状态,并保留 default 处理未知状态。
枚举列出具名状态常量;结构体定义说明每个传感器对象有哪些成员。
sensor_make(42, 40) 创建局部结构体 result,设置两个整数和初始状态,再按值返回结构体。
&sensor 是 main 中对象的地址。sensor_classify 收到一个指针,先检查是否为 NULL。
sensor->raw 为 42,sensor->threshold 为 40;42 大于等于 40,所以函数返回 SENSOR_HIGH。
sensor.state 接收返回值。状态与 SENSOR_HIGH 相等,所以显示 HIGH;该具名常量的示例值是 1。
sensor,但它是独立的指针变量;main 中的 sensor 是结构体对象。两者的类型不同。typedef struct Sensor {
int raw;
int threshold;
enum SensorState state;
} Sensor;struct Sensor 是结构体类型名,其中 Sensor 是结构体标签;花括号列出三个成员,每个成员有自己的类型和名称。结束花括号后还要写分号。这个定义描述对象的组成,本身不创建本例中的传感器变量。
这里的 typedef 为这个结构体类型引入别名 Sensor,因此 Sensor item; 与 struct Sensor item; 使用同一个结构体类型。typedef 不创建新变量,也不会自动转换数据或复制对象。
Sensor sensor = { .raw = 42, .threshold = 40, .state = SENSOR_READY };
printf("%d\n", sensor.raw);
sensor.threshold = 45;在 C11 中,.raw = 42 是指定成员的初始化写法;它清楚说明数值属于哪个成员。也可以按照成员声明顺序写 Sensor sensor = {42, 40, SENSOR_READY};。对象初始化时部分成员未列出,会按静态存储期对象的规则初始化;本课为清晰起见显式列出所有成员。
sensor.raw 用点号从结构体对象读取 raw;sensor.threshold = 45; 修改该对象的阈值。成员访问不会自动重新执行分类:如果阈值变化后还要更新状态,应按程序需求再次调用判断函数并保存结果。
Sensor sensor = sensor_make(42, 40);
Sensor *p = &sensor;
p->threshold = 45;
printf("raw=%d\n", (*p).raw);p 保存对象地址,p->threshold 等价于 (*p).threshold:先通过指针取得对象,再访问成员。括号不能随意去掉,因为 . 与 * 的结合方式不同。对结构体对象直接用 .,对有效的结构体指针可用 ->。
sensor_classify(const Sensor *sensor) 通过指针读取对象,const 表示不能通过这个指针修改所指对象的成员;它不保证原对象不会通过其他有效途径改变。函数先检查 NULL,但非空也不自动证明指针仍指向有效对象;调用方仍要保证指针的来源、类型和对象生命周期正确。
主例程中的 sensor_make 返回一个结构体值:函数内的局部对象在返回时提供其值,main 用该值初始化自己的对象。它没有返回局部变量的地址,因此返回后 main 中的对象仍可使用。
Sensor with_threshold(Sensor item, int threshold)
{
item.threshold = threshold;
return item;
}
/* 已有 Sensor sensor 时: */
Sensor changed = with_threshold(sensor, 50);这里传入的是结构体值;item 是函数中的独立形参对象,修改其整数成员不直接改变调用方的 sensor。返回结果被保存到另一个对象 changed。也可以显式写 sensor = with_threshold(sensor, 50); 用返回值更新原对象。
结构体按值赋值会复制其成员的值:如果将来某个成员本身是指针,复制的是地址值,两个结构体中的指针仍可能指向同一对象;这不会自动复制指针所指的数据。通过有效指针修改该数据,也可能影响其他访问者看到的内容。
enum SensorState {
SENSOR_READY = 0,
SENSOR_HIGH = 1,
SENSOR_ERROR = 2
};enum SensorState 是枚举类型,花括号中的标识符是枚举常量。这里显式指定 0、1、2,便于阅读和调试;没有显式指定时,首个默认从 0 开始,后续未指定的值接在前一项之后递增。
枚举提供名称,却不会自动证明外部输入一定是列表中的某个状态。对串口、网络或其他不可信输入,应先验证取值,再决定如何解释;处理状态的 switch 可保留 default 分支,应对非预期值。一个状态对象的表示大小依具体实现,不能把它当作固定宽度的通信字段。
sizeof(Sensor) 的结果是该实现中对象占用的字节数,类型为 size_t,可以使用 printf("%zu\n", sizeof(Sensor)); 在本机查看。编译器为满足成员对齐等要求,可能在成员之间或结构体末尾加入填充;成员类型、大小、对齐与目标平台和工具链有关,不应把本机数字当成所有芯片的结果。
通信协议需要明确每个字段的位宽、编码、顺序和字节序;枚举的大小与结构体填充也不能作为协议约定。应依据协议逐字段编码和解码,校验长度与取值;如果使用 <stdint.h> 的定宽整数,也应核对目标实现提供了所需类型并自行规定字节顺序。
结构体可以组织配置、采样值和状态,但不能仅凭一份普通 C 结构体推断某芯片寄存器的地址、宽度或访问语义。涉及内存映射寄存器时,应使用目标芯片厂商提供的定义并核对手册、工具链与读写限制。
| 现象 | 原因线索 | 检查方法 |
|---|---|---|
| 成员访问编译报错 | 对结构体指针用了 .,或对对象用了 -> | 先查变量类型;对象用点号,有效指针用箭头。 |
| 修改成员后状态没有变 | 状态只是保存的字段,不会自动更新 | 按需要再次执行分类并保存返回值。 |
| 函数调用崩溃或异常 | 指针为空、失效或类型不正确 | 追踪对象生命周期与地址来源,避免解引用无效指针。 |
| 通信双方解读不同 | 直接发送结构体内存,忽略填充、宽度或字节序 | 对照协议逐字段编码、校验并在两端测试。 |
| 状态显示不符合预期 | 只处理了部分枚举值 | 明确 ERROR 与未知值的处理路径。 |
把主例程 sensor_make(42, 40) 改成 sensor_make(39, 40),预测输出的状态和编号。
39 小于 40,返回 SENSOR_READY,因此显示 state=READY、code=0;第一行相应显示 raw=39 threshold=40。
若 Sensor *p = &sensor; 且 p 有效,读取阈值的 p->threshold 还可以怎样写?
(*p).threshold。先解引用指针得到结构体对象,再用点号访问成员;*p.threshold 不是同一个分组。
在函数外创建 Sensor changed = with_threshold(sensor, 50);。如果忽略结构体内部的指针成员,本例调用完成后原对象 sensor.threshold 会自动变成 50 吗?
不会。函数的结构体形参是独立对象,返回的新结构体初始化 changed;只有在调用方将返回值赋给 sensor 等操作后,原对象成员才会随之改变。
收到一个未经验证的状态数字时,为什么不能只依靠枚举声明就认为它肯定是 READY、HIGH 或 ERROR?
枚举为一些值提供名称,不能替代对外部输入的校验。应先检查字段宽度、长度和数值,再映射到具名状态,并设计未知值或错误路径。
.、-> 和 const Sensor * 的用途。下一课学习位运算,用普通变量模拟硬件寄存器中的位设置和检查。
平台的类型大小、字节序和寄存器访问方式应以目标工具链、协议与芯片资料为准。