2、基本数据类型与位宽:int、long、char的跨平台陷阱

各位同学,咱们今天聊一个老生常谈、但又特别容易翻车的话题——基本数据类型的位宽问题。

我记得刚入行那会儿,在ARM7上写了一段代码,用int存了一个计数器,逻辑跑得稳稳的。后来项目移植到一颗16位的MSP430上,程序直接崩溃。查了两天才发现,int从32位变成了16位,溢出得悄无声息。嗯,从那以后,我对基本数据类型就多了一份敬畏。

2.1 标准C的“模糊约定”

C语言标准对基本类型只做了最小范围约束,没规定具体位宽。你想想看,这给跨平台埋了多少坑?

类型 C标准最小范围 常见32位平台 常见16位平台 常见64位平台
char 至少8位 8位 8位 8位
short 至少16位 16位 16位 16位
int 至少16位 32位 16位 32位
long 至少32位 32位 32位 64位(LP64)或32位(LLP64)
long long 至少64位 64位 64位 64位

看到没?int在16位平台上是16位,在32位和64位平台上通常是32位。而long更离谱——Windows上64位系统它还是32位,Linux上就变成64位了。我当年第一次在Windows和Linux之间移植代码时,就被这个差异坑得不轻。

2.2 为什么不能用“我以为”的位宽

很多新手写代码时,心里默认int就是32位。这种“我以为”的思维,在嵌入式领域特别危险。

举个例子:

// 危险写法:依赖int为32位
int counter = 0;
while (counter < 100000) {  // 100000超过了16位int的最大值32767
    // 做点什么
    counter++;
}

这段代码在16位平台上,counter到32767就溢出了,循环永远不会结束。我在一个电机控制项目里遇到过类似问题,当时电机一直转不停,差点把机械结构搞坏。

⚠️ 警告: 永远不要假设基本类型的位宽。编译器、芯片架构、操作系统都可能改变它。

2.3 解决方案:<stdint.h>固定宽度类型

C99标准引入了<stdint.h>,提供了位宽明确的数据类型。我个人习惯,所有新项目第一件事就是包含这个头文件。

类型 位宽 取值范围 适用场景
int8_t 8位有符号 -128 ~ 127 小范围计数、状态机
uint8_t 8位无符号 0 ~ 255 字节数据、标志位
int16_t 16位有符号 -32768 ~ 32767 中等范围计数
uint16_t 16位无符号 0 ~ 65535 传感器原始数据
int32_t 32位有符号 -2^31 ~ 2^31-1 通用整数
uint32_t 32位无符号 0 ~ 2^32-1 时间戳、大范围计数
int64_t 64位有符号 -2^63 ~ 2^63-1 高精度时间、大数运算
uint64_t 64位无符号 0 ~ 2^64-1 文件大小、ID生成

用固定宽度类型改写上面的例子:

// 安全写法:位宽明确
#include <stdint.h>

uint32_t counter = 0;
while (counter < 100000) {
    // 不管在什么平台上,counter都是32位
    counter++;
}

代码的可读性也提升了——看到uint32_t,你立刻知道这个变量最大能到42亿多。看到int16_t,你就知道它只能存3万出头。

2.4 特殊类型:size_tptrdiff_t

除了固定宽度类型,<stddef.h>里还有两个特殊类型值得注意。

size_t是无符号类型,用来表示内存大小。在16位平台上是16位,32位上是32位,64位上是64位。说白了,它跟着地址总线宽度走。

// 正确做法:用size_t表示大小
#include <stddef.h>
#include <string.h>

void process_buffer(void* buf, size_t len) {
    // len自动适配平台位宽
    uint8_t* byte_buf = (uint8_t*)buf;
    for (size_t i = 0; i < len; i++) {
        // 处理每个字节
    }
}

ptrdiff_t是有符号类型,表示两个指针的差值。同样,它的位宽跟着地址总线走。

💡 提示: 我建议在定义循环变量、数组索引、内存大小时,优先使用size_t。它比int更安全,也更符合语义。

2.5 避坑指南

我曾经在项目中踩过这些坑,分享给大家:

  • 不要用int做位运算——位宽不确定,结果可能出乎意料。用uint32_t这类固定类型。
  • 结构体成员对齐问题——不同平台对int的对齐要求不同。用固定宽度类型后,对齐行为更可预测。
  • 序列化/反序列化——网络协议、文件格式中,一定要用固定宽度类型。否则两端平台不同,数据就乱了。
  • printf格式化——uint32_t%uPRIu32int64_t%lldPRId64。别搞混了。
// 安全的序列化示例
#include <stdint.h>
#include <stdio.h>

typedef struct {
    uint32_t id;
    uint16_t type;
    uint8_t  status;
    uint8_t  reserved;  // 对齐填充
} __attribute__((packed)) Packet;

void send_packet(Packet* pkt) {
    // 直接发送结构体,位宽和布局都是确定的
    send_bytes((uint8_t*)pkt, sizeof(Packet));
}

嗯,这里用了__attribute__((packed))来取消对齐填充,确保结构体在内存中是紧凑排列的。不过要注意,取消对齐可能会影响访问效率,在性能敏感的场景要权衡。

2.6 总结一下

说白了,跨平台数据类型的核心就一句话:<stdint.h>里的固定宽度类型,别用intlong这些模糊类型

我个人的编码规范里,只有三种情况会用基本类型:

  1. 循环变量——用size_t
  2. 字符数据——用char(但注意它可能是有符号或无符号的)
  3. 与标准库接口交互——比如fgetc()返回int

其他所有整数变量,一律用uint8_tint32_t这种。习惯养成之后,跨平台移植就是改改编译选项的事,不用再熬夜查bug了。

下一节我们聊聊字节序的问题——大端小端,又一个让嵌入式工程师头疼的话题。