第4章 基本数据类型使用:隐式转换、stdint.h与char算术运算
数据类型这事儿,说大不大,说小不小。我见过太多项目,最后出bug就出在类型上。嵌入式系统里,资源紧张,类型用不对,轻则结果不对,重则直接跑飞。今天咱们就把这块掰开揉碎了聊。
4.1 隐式类型转换——看不见的陷阱
C语言有个“好心”的机制:当不同类型的数据混在一起运算时,编译器会自动做类型转换。但这份好心,在嵌入式里常常办坏事。
/* 问题代码 */
uint16_t a = 1;
int16_t b = -2;
if (a + b > 0) {
/* 你以为会执行?实际上不会! */
}
为什么会这样?因为 a + b 时,b 被隐式转成了 uint16_t,-2 变成了 65534。1 + 65534 = 65535,当然大于0。但逻辑完全错了。
我在项目中遇到过类似问题。一个电机控制程序,电流采样值用了有符号,阈值用了无符号。结果过流保护总是不触发,差点烧了驱动板。查了两天才发现是类型转换搞的鬼。
我个人习惯的做法是:
- 所有比较操作,两边类型必须一致
- 所有赋值操作,右侧类型必须能无损赋给左侧
- 实在需要转换,用
(int32_t)或(uint32_t)写清楚
4.2 stdint.h——给你的类型一个确定的大小
你想想看,int 在8位单片机上是16位,在32位处理器上是32位,在64位系统上又是64位。同一个代码,换个平台就出问题,这谁受得了?
MISRA C强制要求使用 stdint.h 中定义的确切宽度类型。说白了,就是让每个变量的大小在任意平台上都一样。
| 传统类型 | stdint.h 替代 | 宽度(位) | 范围 |
|---|---|---|---|
| char | int8_t / uint8_t | 8 | -128~127 / 0~255 |
| short | int16_t / uint16_t | 16 | -32768~32767 / 0~65535 |
| int | int32_t / uint32_t | 32 | -2^31~2^31-1 / 0~2^32-1 |
| long long | int64_t / uint64_t | 64 | -2^63~2^63-1 / 0~2^64-1 |
#include <stdint.h>,然后所有变量声明都用 uint32_t、int16_t 这种。团队里统一约定,代码审查时一眼就能看出问题。
我曾经在一个跨平台项目里吃过亏。代码在ARM上跑得好好的,移植到RISC-V上,int 宽度没变,但 long 从32位变成了64位。结构体对齐全乱了,通信协议解析全错。从那以后,我团队里禁止使用 int、long 这些模糊类型。
4.3 禁止char进行算术运算
很多人觉得 char 就是8位整数,拿来算算没啥。嗯,这里要注意——char 在C标准里是三种类型之一:char、signed char、unsigned char。它到底是有符号还是无符号?由编译器决定。
这就麻烦了。你在gcc上编译,char 可能是 signed;在ARMCC上,它可能是 unsigned。同一个代码,换个编译器结果就变了。
/* 危险操作 */
char c = 0x80;
c = c + 1; /* 结果取决于char的符号性 */
/* 正确做法 */
uint8_t u = 0x80;
u = u + 1; /* 结果确定:0x81 */
MISRA C明确禁止对 char 类型做算术运算。它的职责就是存字符,不是做计算。需要8位整数?用 uint8_t 或 int8_t。
char 做循环变量和数组索引。从IAR换到Keil后,char 的符号性变了,循环条件全乱套。改了一个星期才改完。
那什么时候可以用 char?只有一种情况:处理文本字符串。比如 char *p = "hello"; 这种。但即使这样,我建议用 const char * 来强调它是只读的字符串。
4.4 知识体系总览
下面这张图把本章的核心逻辑串起来了。你可以把它当作代码审查时的检查清单。
4.5 实战建议与代码审查清单
说了这么多理论,最后给点实在的。我团队里做代码审查时,会逐条检查下面这些项:
- 所有整数类型是否来自
stdint.h?——不允许出现裸的int、short、long - 有没有
char参与算术运算?——循环变量、索引、计数器都不行 - 有符号与无符号混用时,是否显式转换?——用
(int32_t)或(uint32_t)写清楚 - 赋值操作是否可能丢失精度?——比如
uint32_t赋给uint16_t,必须检查范围 - 位运算操作数是否都是无符号类型?——有符号数的位运算结果是实现定义的
stdint.h 明确宽度,用显式转换消除歧义,把 char 留给字符串,把算术交给 uint8_t。
嗯,这一章就到这儿。记住这些规则,写出来的代码至少能少一半的移植问题。