编译并运行
gcc -std=c11 -Wall -Wextra -g -O0 main.c sensor.c -o sensor_demo.exe
./sensor_demo.exeC foundations / Lesson 08
把上一课的采样计算拆成主程序、头文件和实现文件。亲手编译链接,再用 GDB 在函数入口停下,查看传入的长度和第一个元素。
如果每个功能都挤在 main 里,程序变长后就很难定位问题。我们可以给采样计算一个清晰的接口,让主程序调用它;具体算法则放在单独的源文件里。
.h 中的函数声明与 .c 中的函数定义。一个含 main.c、sensor.c、sensor.h 的项目;一份 GDB 调试记录,写下传入的样本数、首项和函数返回值。
新建一个工作文件夹,把以下三个文件放在同一目录。不要把文件内容拼接在一起:每个代码块对应一个文件名。
sensor-demo/
├── main.c 主程序:准备数据、调用函数、打印结果
├── sensor.h 接口:声明函数和需要的类型
└── sensor.c 实现:计算平均值、判断阈值例如主程序需要知道 sensor_mean 的参数和返回类型,但无需把计算循环重写一遍。头文件提供声明,源文件提供定义;三份文件保存后一起参与构建。
头文件说明函数可以怎样调用。将下方内容保存为 sensor.h:
#ifndef SENSOR_H
#define SENSOR_H
#include <stddef.h>
double sensor_mean(const int samples[], size_t count);
int sensor_is_high(double value, double threshold);
#endifsize_t 是元素数量使用的类型,由 <stddef.h> 提供。两个声明末尾都有分号:它们告诉调用方参数类型和返回类型,并没有提供函数体。
#ifndef、#define 与 #endif 组成包含保护:在同一个源文件的预处理过程中,重复包含该头文件时,保护宏可避免重复展开内容。名称要在项目中保持唯一且前后一致。
samples 指向一段可读取的 int 数据,count 是实际要读取的元素数。const 表示函数不会通过此参数修改数组元素,调用方仍须传递正确长度。保存为 sensor.c。这个文件包含自己的头文件,帮助编译器对照声明与定义。
#include "sensor.h"
double sensor_mean(const int samples[], size_t count)
{
if (samples == NULL || count == 0) {
return 0.0;
}
double sum = 0.0;
for (size_t i = 0; i < count; i++) {
sum += samples[i];
}
return sum / (double)count;
}
int sensor_is_high(double value, double threshold)
{
return value >= threshold;
}sensor_mean 先处理空指针或零元素的情况,再遍历样本。这里为了练习约定两种情况返回 0.0;它无法区分“没有样本”和“样本平均值恰好为零”。真实系统可另设状态返回值,不应把这个简化接口当作完整故障处理。
sensor_is_high 比较两个数:大于或等于阈值时返回 1,否则返回 0。函数中的 count 必须由调用方按实际数组长度传入;空指针检查不能证明非空指针一定有效,也不能发现过大的长度。
保存为 main.c。这里的样本是固定数据,不需要输入,便于先预测结果。
#include <stdio.h>
#include <stddef.h>
#include "sensor.h"
int main(void)
{
int samples[] = {18, 21, 24, 20};
size_t count = sizeof samples / sizeof samples[0];
double mean = sensor_mean(samples, count);
int high = sensor_is_high(mean, 22.0);
printf("count=%zu\n", count);
printf("mean=%.2f C\n", mean);
printf("status=%s\n", high ? "HIGH" : "NORMAL");
return 0;
}平均值是 (18 + 21 + 24 + 20) / 4 = 20.75。由于 20.75 小于阈值 22.0,high 为 0。条件运算符 high ? "HIGH" : "NORMAL" 根据条件选择一个字符串,供 %s 输出。
注意 #include "sensor.h" 使用引号,引用本项目头文件;#include <stdio.h> 引入标准库头文件。它们告诉编译器声明,编译时仍需把包含定义的 sensor.c 一起构建。
打开上一课使用的终端,进入三个文件所在目录,并先运行 gcc --version。两种环境命令稍有不同:
gcc -std=c11 -Wall -Wextra -g -O0 main.c sensor.c -o sensor_demo.exe
./sensor_demo.exegcc -std=c11 -Wall -Wextra -g -O0 main.c sensor.c -o sensor_demo
./sensor_demo-g 生成调试信息,-O0 在本例中关闭优化,便于逐行观察;-Wall -Wextra 开启常用警告。成功输出:
count=4
mean=20.75 C
status=NORMAL修改任一源文件后,先保存并重新执行编译命令,再运行新生成的程序。调试时也要使用重新编译后的可执行文件。
可以把“一次命令”理解为几个阶段:预处理器展开头文件;编译器将每个 .c 处理为目标代码;链接器把主程序对函数的引用与另一个文件中的定义连接起来。下图是帮助理解的简化流程:
main.c + sensor.h ──编译──> main.o ┐
├─链接─> sensor_demo
sensor.c + sensor.h ─编译──> sensor.o ┘想观察中间文件,可在同一目录依次执行:
gcc -std=c11 -Wall -Wextra -g -O0 -c main.c
gcc -std=c11 -Wall -Wextra -g -O0 -c sensor.c
gcc main.o sensor.o -o sensor_demoWindows 的 GCC 通常生成 sensor_demo.exe,也可以把最后一条的输出名明确写为 sensor_demo.exe;Ubuntu 使用 ./sensor_demo 运行。-c 到生成目标文件为止,不进行最终链接。头文件在本例中被源文件包含,不需要把 sensor.h 单独当作一个源文件链接。
只编译 main.c 即使能看到头文件声明,链接时仍需要找到函数定义;若漏掉 sensor.c 或对应目标文件,就会出现类似 undefined reference 的链接错误。
先在终端运行 gdb --version。如果命令不存在,Windows 的 MSYS2 UCRT64 终端可安装 pacman -S mingw-w64-ucrt-x86_64-gdb;Ubuntu 可用 sudo apt update 后执行 sudo apt install gdb。以系统包管理器与当前环境实际提示为准。
确保上一节的可执行文件用 -g -O0 重新生成。在终端启动调试器:
gdb ./sensor_demo.exegdb ./sensor_demo进入后出现 (gdb) 提示符。以下每行是一个命令,提示符本身不需要输入:
(gdb) break sensor_mean
(gdb) run
(gdb) print count
(gdb) print samples[0]
(gdb) finish
(gdb) continue
(gdb) quit$1、$2 因会话不同而变化。finish 运行到当前函数返回;返回值应为 20.75。continue 继续到程序结束,再用 quit 退出 GDB。如果想逐行观察,可在断点处输入 next;它执行当前行并停在下一位置。变量只有执行到初始化之后才适合观察其初始值。例如 sum 的声明执行前,不应把它显示出的内容当作有效结果。不同 GDB 版本可能显示不同的附加提示。
数组传参时,函数拿到地址和调用方给的数量。主程序在数组定义所在作用域计算 sizeof samples / sizeof samples[0],得到实际的 4;sensor_mean 用 i < count 控制访问。
若调用者把数量写成 5,而数组仍只有 4 项,函数没有足够信息自动恢复正确长度;继续读取就会越界。空指针和零长度分支只是基本检查。真正的可复用模块还需要清晰的错误状态、数据类型范围、缓冲区约定和测试。
以后采集 ADC 或串口数据时,同样需要记录缓冲区容量、已有数据量和模块接口。电脑上的例程帮助理解组织方式;板卡上的启动文件、外设和工具链还需要根据具体项目学习。
| 现象 | 可能原因 | 处理 |
|---|---|---|
sensor.h: No such file or directory | 文件名拼错或不在当前工作目录。 | 用 ls 查看三个文件,核对拼写和大小写。 |
undefined reference to sensor_mean | 已看到声明,但链接时没有对应定义。 | 检查 sensor.c 是否参与命令,核对名称和参数声明。 |
| 重复定义函数 | 不小心同时包含源文件又单独编译该源文件。 | 在本例中包含 sensor.h,并单独编译 sensor.c。 |
| GDB 看不到源码或变量 | 可执行文件未带调试信息,或源文件路径不同。 | 在当前目录以 -g -O0 重新构建,并确认使用了新程序。 |
| 修改后仍看到旧结果 | 源码已保存但没有重新编译,或运行了其他目录的程序。 | 核对目录、重新编译、确认执行路径。 |
只改 main.c 中传给 sensor_is_high 的阈值。平均值和状态分别是什么?
平均值仍为 20.75;因为 20.75 ≥ 20.0,状态变为 HIGH。保存并重新编译两个源文件后再运行。
主程序需要调用 sensor_mean。它应该包含头文件,还是把 sensor.c 的源代码直接包含进来?为什么?
包含 sensor.h 获取声明,并在编译/链接命令中加入 sensor.c。这样主程序和实现共享接口,避免把定义重复编进来。
在 sensor_mean 断点处,使用 print count 和 print samples[0]。结果是什么?
数量为 4,第一个值为 18。断点处显示的行号和 GDB 结果编号可能不同。
在当前约定下,count 为 0 时 sensor_mean 返回什么?这种设计有何不足?
返回 0.0,不会执行读取循环。但调用方无法区分空数据与真实平均值为零;生产接口需要单独表达错误状态。
sensor_mean 设置断点,观察数量 4、首项 18 和返回值 20.75。你已经走完这组 C 基础课。接下来可以继续深入指针、结构体、位运算和调试,并逐步把这些能力带到 STM32 项目中。