第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。但逻辑完全错了。

我在项目中遇到过类似问题。一个电机控制程序,电流采样值用了有符号,阈值用了无符号。结果过流保护总是不触发,差点烧了驱动板。查了两天才发现是类型转换搞的鬼。

MISRA C规则:禁止有符号与无符号类型之间的隐式转换。必须显式写出转换意图。

我个人习惯的做法是:

  • 所有比较操作,两边类型必须一致
  • 所有赋值操作,右侧类型必须能无损赋给左侧
  • 实在需要转换,用 (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 知识体系总览

下面这张图把本章的核心逻辑串起来了。你可以把它当作代码审查时的检查清单。

基本数据类型使用知识体系 基本数据类型使用——核心知识体系 基本数据类型使用 隐式类型转换 使用stdint.h类型 禁止char算术运算 有符号 ↔ 无符号混用 整数提升规则 赋值时精度丢失 int8_t / uint8_t int16_t / uint16_t int32_t / uint32_t int64_t / uint64_t char符号性由编译器决定 算术运算结果不可移植 只能用uint8_t替代 代码审查要点 检查所有类型转换是否显式 | 确认所有整数类型来自stdint.h | 禁止char参与任何算术运算

4.5 实战建议与代码审查清单

说了这么多理论,最后给点实在的。我团队里做代码审查时,会逐条检查下面这些项:

  1. 所有整数类型是否来自 stdint.h?——不允许出现裸的 int、short、long
  2. 有没有 char 参与算术运算?——循环变量、索引、计数器都不行
  3. 有符号与无符号混用时,是否显式转换?——用 (int32_t) 或 (uint32_t) 写清楚
  4. 赋值操作是否可能丢失精度?——比如 uint32_t 赋给 uint16_t,必须检查范围
  5. 位运算操作数是否都是无符号类型?——有符号数的位运算结果是实现定义的
一句话总结:类型用对,bug减半。用 stdint.h 明确宽度,用显式转换消除歧义,把 char 留给字符串,把算术交给 uint8_t。

嗯,这一章就到这儿。记住这些规则,写出来的代码至少能少一半的移植问题。