4、数据类型与字节对齐:不同平台下基本数据类型的大小(sizeof)、字节序(大端/小端)问题及处理
跨平台开发,说白了就是让你的代码能在不同机器上跑得一样好。但现实很骨感——同样的 int,在 A 平台占 4 字节,到 B 平台可能就变成 2 字节了。我早年做嵌入式开发时,就吃过这个亏。一个结构体在 x86 上调试得好好的,移植到 ARM 上直接崩了。查了一整天,最后发现是字节对齐搞的鬼。
这一章,咱们就把数据类型的大小、字节序、对齐这三个坑,一个一个填平。
4.1 基本数据类型的大小——别信直觉,信 sizeof
C 标准只规定了各类型的最小范围,没规定具体字节数。比如 int 至少 16 位,但实际可能是 16、32 甚至 64 位。你想想看,这要是直接写死 4,换个平台不就炸了?
| 类型 | 典型 32 位平台 | 典型 64 位平台 | 常见嵌入式平台 |
|---|---|---|---|
| char | 1 字节 | 1 字节 | 1 字节 |
| short | 2 字节 | 2 字节 | 2 字节 |
| int | 4 字节 | 4 字节 | 2 或 4 字节 |
| long | 4 字节 | 8 字节 | 4 字节 |
| long long | 8 字节 | 8 字节 | 8 字节 |
| float | 4 字节 | 4 字节 | 4 字节 |
| double | 8 字节 | 8 字节 | 8 字节 |
| 指针 | 4 字节 | 8 字节 | 2 或 4 字节 |
核心原则:永远不要假设基本类型的大小。用 sizeof 获取,用 <stdint.h> 中的定长类型(如 int32_t、uint64_t)来保证跨平台一致性。
// 错误做法:假设 int 是 4 字节
int buffer[1024];
memset(buffer, 0, 4096); // 换个平台就错了
// 正确做法:用 sizeof
int buffer[1024];
memset(buffer, 0, sizeof(buffer));
// 更安全的做法:用定长类型
#include <stdint.h>
int32_t buffer[1024];
memset(buffer, 0, sizeof(buffer));
我的习惯:只要涉及跨平台,我优先用 int32_t、uint16_t 这类定长类型。结构体里的字段更是如此——你永远不知道下一个平台会是什么。
4.2 字节序——大端与小端的爱恨情仇
字节序,说白了就是多字节数据在内存里怎么排。大端(Big-Endian)把高位字节放低地址,小端(Little-Endian)把低位字节放低地址。x86 是小端,网络协议大多是大端。
为什么会这样?历史原因。不同厂商各玩各的,最后就分裂了。我当年做网络协议栈时,就遇到过从网络收到一个 uint32_t,直接当本地整数用,结果数值完全不对——忘了做字节序转换。
// 检测当前平台的字节序
#include <stdio.h>
#include <stdint.h>
int is_little_endian() {
uint16_t test = 0x0102;
uint8_t *p = (uint8_t *)&test;
return p[0] == 0x02; // 低地址存低位,就是小端
}
int main() {
if (is_little_endian()) {
printf("小端平台\n");
} else {
printf("大端平台\n");
}
return 0;
}
我曾经踩过的坑:在 x86 上写了一个二进制文件解析器,一切正常。把文件拿到 PowerPC 机器上读,全乱码了。原因就是文件里存的整数是小端序,而 PowerPC 是大端。从那以后,我所有跨平台二进制格式都统一用网络字节序(大端)。
处理字节序的标准做法:
- 网络传输:用
htonl()、htons()、ntohl()、ntohs()系列函数 - 文件存储:统一用大端序,或者文件头里标记字节序
- 内存共享:用联合体(union)做字节序无关的访问
// 字节序无关的 uint32_t 读写
#include <stdint.h>
uint32_t read_be32(const uint8_t *buf) {
return ((uint32_t)buf[0] << 24) |
((uint32_t)buf[1] << 16) |
((uint32_t)buf[2] << 8) |
((uint32_t)buf[3]);
}
void write_be32(uint8_t *buf, uint32_t val) {
buf[0] = (val >> 24) & 0xFF;
buf[1] = (val >> 16) & 0xFF;
buf[2] = (val >> 8) & 0xFF;
buf[3] = val & 0xFF;
}
4.3 字节对齐——结构体的隐藏开销
字节对齐,说白了就是编译器为了让 CPU 访问更快,会在结构体成员之间插入填充字节。你写个结构体,实际占用的内存可能比你想象的大得多。
// 看看这个结构体实际占多大
struct misaligned {
char a; // 1 字节
int b; // 4 字节
short c; // 2 字节
};
// 你以为 sizeof = 1 + 4 + 2 = 7?
// 实际上在 32 位平台上,sizeof = 12!
为什么会这样?因为 int 需要 4 字节对齐,所以 a 后面会填充 3 个字节。然后 short 后面再填充 2 个字节,让整个结构体大小是 4 的倍数。
| 成员 | 偏移 | 大小 | 填充 |
|---|---|---|---|
| char a | 0 | 1 | 3 字节(对齐 int) |
| int b | 4 | 4 | 0 |
| short c | 8 | 2 | 2 字节(对齐结构体) |
| 总计 | 12 |
优化技巧:把大的成员往前放,小的往后放,可以减少填充。我习惯在定义结构体时,按成员大小降序排列——这样通常能得到最小的内存占用。
// 优化后的结构体
struct aligned {
int b; // 4 字节,放前面
short c; // 2 字节
char a; // 1 字节
// 只需要填充 1 字节,总共 8 字节
};
// sizeof = 8,比之前省了 4 个字节
4.4 跨平台数据结构的实战策略
好了,理论说完了,咱们看看实际怎么干。我个人总结了一套「三板斧」:
- 用定长类型:所有跨平台结构体,字段类型用
int32_t、uint16_t等,别用int、long - 手动控制对齐:用
#pragma pack或__attribute__((packed))取消自动填充 - 序列化/反序列化:写专门的函数,把结构体转成字节流,统一用大端序
// 跨平台安全的网络包结构体
#include <stdint.h>
#pragma pack(push, 1) // 取消自动对齐
typedef struct {
uint32_t magic; // 魔数,大端序
uint16_t length; // 数据长度,大端序
uint8_t type; // 包类型
uint8_t reserved; // 保留字节
uint8_t data[0]; // 柔性数组,实际数据
} PacketHeader;
#pragma pack(pop)
// 序列化:主机字节序转网络字节序
void packet_serialize(PacketHeader *hdr, uint8_t *buf) {
uint32_t magic = htonl(hdr->magic);
uint16_t len = htons(hdr->length);
memcpy(buf, &magic, 4);
memcpy(buf + 4, &len, 2);
buf[6] = hdr->type;
buf[7] = hdr->reserved;
}
注意:#pragma pack 虽然好用,但会降低内存访问效率。如果性能敏感,建议在结构体里手动填充对齐字节,而不是依赖编译器。
4.5 知识体系总览
下面这张图,把本章的核心知识点串起来了。你可以把它当作一个检查清单——写跨平台代码时,对着过一遍,基本不会漏。
嗯,到这里,数据类型和字节对齐的核心内容就讲完了。记住一句话:跨平台不是靠运气,是靠规范。每次写结构体之前,先想想它会在什么平台上跑,再动手。
本章核心要点:
- 用
sizeof和<stdint.h>消除类型大小差异 - 网络传输和文件存储统一用大端序
- 手动控制结构体对齐,避免隐式填充
- 写序列化/反序列化函数,彻底隔离平台差异
公众号:蓝海资料掘金营,微信deep3321