4、数据类型与字节对齐:不同平台下的基本数据类型大小、字节序 (Endianness)、对齐与填充
这一章,我们来聊聊跨平台开发里最基础、也最容易踩坑的话题——数据类型、字节序和对齐。说实话,我刚开始做嵌入式开发那会儿,觉得这些不过是编译器的事,跟我写代码有什么关系?直到有一次,我在ARM和x86之间传结构体数据,结果解析出来全是乱码……嗯,从那以后,我再也不敢小看这些“底层细节”了。
4.1 不同平台下的基本数据类型大小
你想想看,同样是 int,在16位、32位、64位系统上,大小可能完全不一样。C语言标准只规定了最小范围,没规定具体字节数。这就导致了跨平台移植时,数据大小不一致的问题。
| 数据类型 | 16位系统 | 32位系统 | 64位系统 |
|---|---|---|---|
| char | 1字节 | 1字节 | 1字节 |
| short | 2字节 | 2字节 | 2字节 |
| int | 2字节 | 4字节 | 4字节 |
| long | 4字节 | 4字节 | 8字节 |
| long long | 8字节 | 8字节 | 8字节 |
| float | 4字节 | 4字节 | 4字节 |
| double | 8字节 | 8字节 | 8字节 |
| 指针 | 2字节 | 4字节 | 8字节 |
我个人习惯,在跨平台代码里,尽量使用 stdint.h 中定义的定长类型,比如 int32_t、uint64_t。这样不管换到什么平台,大小都是确定的。
#include <stdint.h>
int32_t counter; // 保证4字节,不管在哪个平台
uint64_t timestamp; // 保证8字节
int 和 long 来定义需要固定大小的数据了。用 int32_t 这类类型,代码可读性更好,移植也更省心。
4.2 字节序 (Endianness)
字节序,说白了就是多字节数据在内存里怎么排列。大端模式(Big-Endian)把高位字节放在低地址,小端模式(Little-Endian)把低位字节放在低地址。x86 和大多数 ARM 都是小端,而网络协议通常用大端。
我在项目中遇到过一个问题:两个设备通过串口通信,一个用 ARM(小端),一个用 PowerPC(大端)。发送方把 0x12345678 直接写进缓冲区,接收方读出来变成了 0x78563412。数据全乱了。
怎么判断当前平台的字节序?写个小函数就行:
#include <stdio.h>
int is_little_endian() {
unsigned int x = 0x1;
return *(unsigned char*)&x == 1;
}
int main() {
if (is_little_endian()) {
printf("小端模式\n");
} else {
printf("大端模式\n");
}
return 0;
}
跨平台传输数据时,我建议统一使用网络字节序(大端)。发送前用 htonl()、htons() 转换,接收后用 ntohl()、ntohs() 还原。这样不管两端是什么平台,数据都不会乱。
uint32_t 和 uint16_t,直接通过 memcpy 发送。结果接收方解析出来全是错的。后来才发现,不仅字节序要转换,结构体里的填充字节也导致了问题。所以,跨平台传输数据,最好用序列化方式,别直接传结构体。
4.3 对齐与填充
对齐,是 CPU 访问内存时的一种优化策略。CPU 读取对齐的数据,一次就能读完;如果没对齐,可能需要读两次,甚至报错。编译器会在结构体成员之间插入填充字节,保证每个成员都对齐到它的自然边界。
举个例子:
struct Example {
char a; // 1字节
int b; // 4字节
short c; // 2字节
};
你猜这个结构体占多少字节?不是 1+4+2=7,而是 12 字节。为什么?因为 int b 需要 4 字节对齐,所以 char a 后面会填充 3 个字节。然后 short c 需要 2 字节对齐,但结构体整体大小要对齐到最大成员(4字节)的整数倍,所以末尾再填充 2 个字节。
| 成员 | 偏移 | 大小 | 填充 |
|---|---|---|---|
| char a | 0 | 1 | 3字节填充 |
| int b | 4 | 4 | 无 |
| short c | 8 | 2 | 2字节填充 |
| 总大小 | 12字节 | ||
如果你希望结构体紧凑一点,可以用 #pragma pack(1) 取消对齐:
#pragma pack(1)
struct PackedExample {
char a;
int b;
short c;
};
#pragma pack()
// 此时 sizeof(PackedExample) == 7
#pragma pack。我一般只在网络协议或文件格式解析时,才用紧凑结构体。
4.4 知识体系结构图
下面这张图,帮你理清本章的核心逻辑:
4.5 实战建议
说了这么多,总结几条我自己的经验:
- 用定长类型:
int32_t、uint64_t这些,别用int、long。 - 跨平台传数据:统一用网络字节序,别偷懒。
- 结构体对齐:如果结构体要跨平台传输,要么用
#pragma pack(1),要么写序列化函数。 - 测试:在目标平台上用
sizeof()和offsetof()验证一下,别想当然。
_Static_assert(sizeof(int32_t) == 4, "int32_t must be 4 bytes");。这样编译阶段就能发现问题,不用等到运行时。
好了,这一章的内容就到这里。数据类型、字节序、对齐,这三个东西看似简单,但真要在项目里处理好,还是需要一些经验的。希望你能在实际开发中,少踩我踩过的那些坑。