TWS 应用层的大部分工作,最后还是要落到 C 代码上。这篇整理开发中经常用到、也容易混淆的内容,方便需要时快速回顾。
一、数据类型与类型转换
嵌入式代码经常使用 uint8_t、uint16_t 和 uint32_t,因为它们的位宽是明确的。
int 占多少字节,由芯片架构和编译器决定。在常见的 32 位平台上,int 通常占 4 字节;在部分嵌入式平台上,也可能只占 2 字节。可以使用 sizeof 查看当前平台上的实际大小:
c
printf("int size: %u\n", (unsigned int)sizeof(int));
sizeof 返回的是 size_t 类型。这里先把结果转换成 unsigned int,再使用 %u 输出,适合常见的嵌入式 printf。
普通计算和循环计数可以使用 int。通信协议、Flash 数据、文件格式和寄存器字段需要明确长度时,优先使用固定位宽类型。
类型转换主要注意下面几种情况:
| 情况 | 需要注意什么 |
|---|---|
| 大类型转小类型 | 高位可能被截断 |
| 有符号转无符号 | 负数会变成很大的正数 |
| 无符号与有符号比较 | 有符号数可能先被转成无符号数 |
| 指针类型转换 | 只改变读取内存的方式,不会改变原来的数据 |
注意:有符号数和无符号数比较前,尽量把两边统一成合适的类型。进行指针类型转换时,要确认原内存长度不小于目标类型的大小。例如原数据只有 1 字节,转换成
uint32_t *后却按照 4 字节读取,就会读到不属于它的内存。
二、指针、数组与生命周期
指针保存的是地址。使用前主要检查四件事:
- 指针是否为空。
- 指向的内存是否仍然有效。
- 访问长度是否越界。
- 指针指向的数据类型是否正确。
c
void update_state(uint8_t *state)
{
if (state == NULL) {
return;
}
*state = 1;
}
数组传入函数后通常会退化成指针,函数内部无法再通过 sizeof 得到原数组长度。因此传递缓冲区时,最好同时传入长度。
c
void parse_data(const uint8_t *data, uint16_t len);
也不要返回局部变量的地址。函数退出后,局部变量的生命周期已经结束,原来的地址不能继续使用。
三、几个常见关键字
| 关键字 | 主要作用 |
|---|---|
static |
限制跨文件可见性,或让局部变量保留上次的值 |
const |
限制通过当前对象或指针修改数据 |
volatile |
要求编译器每次实际访问内存,但不保证原子性和线程安全 |
extern |
声明符号定义在其他文件中,由链接器查找 |
const 放在不同位置,含义也不同:
| 写法 | 能否通过指针修改数据 | 指针能否改指向 | 常见叫法 |
|---|---|---|---|
const uint8_t *p1 |
不能 | 可以 | 常量指针 |
uint8_t * const p2 |
可以 | 不能 | 指针常量 |
const uint8_t * const p3 |
不能 | 不能 | 指向常量的指针常量 |
简单记忆:const 在 * 前面,限制指向的数据;const 在 * 后面。
volatile 常用于可能被中断、硬件或其他执行上下文改变的数据。它解决的是编译器优化问题,不能代替锁、临界区或原子操作。
四、内存区域、结构体与大小端
| 区域 | 常见内容 | 主要特点 |
|---|---|---|
| 栈 | 局部变量、函数调用信息 | 自动分配和释放,空间有限 |
| 堆 | 动态申请的内存 | 使用灵活,但要防止泄漏和碎片 |
| 全局区 | 全局变量、静态变量 | 生命周期贯穿程序运行过程 |
| 只读区 | 常量、只读数据 | 通常不允许修改 |
结构体大小不能只把各成员占用的字节数直接相加,这个问题我之前踩过坑。例如下面的成员看起来一共只有 5 字节:
c
typedef struct {
uint8_t type; // 1 字节
uint32_t value; // 4 字节
} packet_t;
printf("size: %u\n", (unsigned int)sizeof(packet_t));
但在部分平台上,sizeof(packet_t) 得到的可能是 8,而不是 5。编译器可能会在 type 后面自动填充 3 个字节,使 value 从合适的地址开始存放。
因此,通信数据不能想当然地直接转换成结构体。原始数据可能只有 5 字节,但程序却按照 8 字节的结构体读取,字段位置也可能对不上。处理协议数据时,按字段的位置和长度分别解析会更稳妥。
例如,协议规定一包数据固定为 5 字节:
| 字节位置 | 含义 |
|---|---|
| 第 0 字节 | 类型 type |
| 第 1~4 字节 | 数值 value,小端格式 |
收到的数据如下:
c
uint8_t data[5] = {
0x01, // type
0x78, 0x56, 0x34, 0x12 // value
};
不建议直接把它强制转换成结构体:
c
packet_t *packet = (packet_t *)data; // 不推荐
可以先检查长度,再按照字段位置分别读取:
c
uint8_t type;
uint32_t value;
if (sizeof(data) >= 5) {
type = data[0];
value = (uint32_t)data[1]
| ((uint32_t)data[2] << 8)
| ((uint32_t)data[3] << 16)
| ((uint32_t)data[4] << 24);
}
解析后,type 等于 0x01,value 等于 0x12345678。这种写法不受结构体填充影响,也明确处理了协议规定的小端顺序。
大小端决定多字节数据在内存中的排列顺序。发送和接收 uint16_t、uint32_t 时,要按照协议规定转换,不能直接假设双方顺序相同。
写在最后
这篇主要整理了 TWS 应用开发中经常接触,也比较容易混淆的几块 C 语言知识。
实际开发中,很多问题并不是语法不会写,而是忽略了数据类型、长度、地址、内存对齐和生命周期等细节。把这些基础问题弄清楚,在阅读代码和排查问题时会更有帮助。后续遇到值得记录的内容,我也会继续整理。