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_tuint64_t。这样不管换到什么平台,大小都是确定的。

#include <stdint.h>

int32_t counter;      // 保证4字节,不管在哪个平台
uint64_t timestamp;   // 保证8字节
我的建议: 别再用 intlong 来定义需要固定大小的数据了。用 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_tuint16_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 知识体系结构图

下面这张图,帮你理清本章的核心逻辑:

数据类型与字节对齐 基本数据类型大小 char / short / int / long float / double / 指针 推荐使用 stdint.h 定长类型 字节序 (Endianness) 大端 (Big-Endian) 小端 (Little-Endian) 网络字节序统一用大端 对齐与填充 自然对齐边界 编译器自动填充 #pragma pack 控制 跨平台移植核心原则 使用定长类型 + 统一字节序 + 避免直接传输结构体 必要时用序列化/反序列化方案

4.5 实战建议

说了这么多,总结几条我自己的经验:

  • 用定长类型int32_tuint64_t 这些,别用 intlong
  • 跨平台传数据:统一用网络字节序,别偷懒。
  • 结构体对齐:如果结构体要跨平台传输,要么用 #pragma pack(1),要么写序列化函数。
  • 测试:在目标平台上用 sizeof()offsetof() 验证一下,别想当然。
一个小技巧: 我写跨平台代码时,会在头文件里加一段编译时断言,确保数据类型大小符合预期。比如:_Static_assert(sizeof(int32_t) == 4, "int32_t must be 4 bytes");。这样编译阶段就能发现问题,不用等到运行时。

好了,这一章的内容就到这里。数据类型、字节序、对齐,这三个东西看似简单,但真要在项目里处理好,还是需要一些经验的。希望你能在实际开发中,少踩我踩过的那些坑。

公众号:蓝海资料掘金营,微信deep3321