# 1. 字节序 (Endianness / 大小端模式)

核心概念:字节序定义了多字节数据类型(例如 int, short, double 等)在内存中是如何存储其字节的顺序。

一个32位的整数 0x01020304 由4个字节组成:0x01, 0x02, 0x03, 0x04

  • 0x01最高有效字节 (Most Significant Byte, MSB)
  • 0x04最低有效字节 (Least Significant Byte, LSB)

内存地址总是从低到高增长。根据将 MSB 还是 LSB 存放在低地址,产生了两种模式:

模式 描述 存储示例 (int 0x01020304)
大端模式 (Big-Endian) 最高有效字节 (MSB) 存放在 最低的内存地址。符合人类阅读习惯。 01 02 03 04
小端模式 (Little-Endian) 最低有效字节 (LSB) 存放在 最低的内存地址。符合计算机处理逻辑。 04 03 02 01

# a. 大端模式 (Big-Endian)

对于 int num = 0x01020304;,其在内存中的存储方式如下:

内存地址 (低 ---> 高)
+--------+--------+--------+--------+
|  0x01  |  0x02  |  0x03  |  0x04  |
+--------+--------+--------+--------+
 0x1000   0x1001   0x1002   0x1003
  • 常见架构:PowerPC, SPARC, 网络字节序 (Network Byte Order)。

# b. 小端模式 (Little-Endian)

对于 int num = 0x01020304;,其在内存中的存储方式如下:

内存地址 (低 ---> 高)
+--------+--------+--------+--------+
|  0x04  |  0x03  |  0x02  |  0x01  |
+--------+--------+--------+--------+
 0x1000   0x1001   0x1002   0x1003
  • 常见架构:x86, x86-64, ARM (大部分模式下)。

# c. 如何判断当前系统的字节序?

可以通过检查一个整数的第一个字节来判断。整数 1 在内存中表示为 0x00000001

  • 在小端系统中,最低地址存放的是 0x01
  • 在大端系统中,最低地址存放的是 0x00
#include <iostream>

void check_endianness() {
    int num = 1;
    // 将int指针强制转换为char指针,指向num的最低地址
    char* ptr = reinterpret_cast<char*>(&num);

    if (*ptr == 1) {
        std::cout << "本系统是 小端模式 (Little-Endian)" << std::endl;
    } else {
        std::cout << "本系统是 大端模式 (Big-Endian)" << std::endl;
    }
}

# 2. 内存对齐 (Memory Alignment)

核心概念:CPU访问内存不是逐字节进行的,而是以块(通常是2, 4, 8字节)为单位。为了让CPU高效地读取数据,编译器会自动将数据存放在特定地址,这个地址通常是其数据类型大小的整数倍。

为什么需要对齐?

  • 性能:对齐的数据可以被CPU在一个总线周期内读取。如果数据未对齐,CPU可能需要两次读取再拼接数据,降低性能。
  • 硬件要求:某些硬件平台(尤其是RISC架构,如ARM)不允许访问未对齐的数据,强行访问会触发硬件异常。

# a. 对齐规则

  1. 成员对齐:结构体(struct)或类(class)的每个成员,其存放的起始地址相对于结构体起始地址的偏移量,必须是其自身大小(或指定对齐值)的整数倍。
  2. 整体对齐:结构体或类的总大小,必须是其最宽的成员(或指定对齐值)大小的整数倍。

示例分析

#include <iostream>

struct MyStruct {
    char a;    // 1字节
    int b;     // 4字节
    short c;   // 2字节
};

int main() {
    // 预期大小: 1 + 4 + 2 = 7
    // 实际大小: 12
    std::cout << "sizeof(MyStruct) = " << sizeof(MyStruct) << std::endl;
}

内存布局

Snipaste_2025-09-07_00-16-45

  • char a: 位于偏移量 0
  • int b: 大小为4,其偏移量必须是4的倍数。因此编译器在 a 后填充3字节,使 b 从偏移量 4 开始。
  • short c: 大小为2,b 结束后偏移量为 8,是2的倍数,可以直接存放。
  • 整体对齐: 结构体最宽成员是 int b (4字节),总大小必须是4的倍数。当前大小为 1 (a) + 3 (pad) + 4 (b) + 2 (c) = 10 字节。为满足4的倍数要求,末尾再填充2字节,最终大小为12字节。

# b. 如何控制对齐 #pragma pack(n)

可以使用预处理指令 #pragma pack(n) 来改变编译器的默认对齐系数。n 可以是 1, 2, 4, 8 等。成员对齐时,将按照 n 和成员自身大小中 较小 的值进行对齐。

#pragma pack(1) // 设置对齐系数为1,即无填充
struct MyStructPacked {
    char a;    // 1字节
    int b;     // 4字节
    short c;   // 2字节
};
#pragma pack() // 恢复默认对齐

// sizeof(MyStructPacked) 的结果将是 1 + 4 + 2 = 7

# 3. 常见导致问题的场景

# a. 网络通信 (字节序问题)

不同架构的计算机字节序可能不同。而网络协议规定字节序为大端模式 (Network Byte Order)

  • 问题:小端机器 (x86) 发送 0x01020304 (内存中为 04 03 02 01),大端服务器接收后会误读为 0x04030201

  • 解决方案:发送前,统一将数据从主机字节序 (Host Order) 转换到网络字节序 (Big-Endian);接收后反向转换。

    • 常用函数htons(), htonl(), ntohs(), ntohl()
      • h 代表 host, to 代表 to, n 代表 network, s 代表 short, l 代表 long。
    #include <arpa/inet.h> // 在Linux/macOS中
    // #include <winsock2.h> // 在Windows中
    
    uint32_t num = 0x01020304;
    uint32_t net_num = htonl(num); // 转换为主机序到网络序
    // send(socket, &net_num, sizeof(net_num), 0);

# b. 文件读写与数据持久化 (字节序与对齐问题)

直接将结构体的二进制内存写入文件,会同时写入字节序和内存对齐产生的填充字节。

MyStruct data;
// file.write(reinterpret_cast<char*>(&data), sizeof(MyStruct)); // 错误做法
  • 问题:
    1. 字节序:在小端机器上写的文件,用大端机器读会出错。
    2. 对齐:不同编译器或编译选项可能导致填充方式不同,造成跨平台/版本解析失败。
  • 解决方案:采用序列化 (Serialization)。逐个成员地进行读写,并为文件格式定义统一的字节序(通常推荐大端)。

# c. 嵌入式开发与硬件交互 (字节序与对齐问题)

直接读写硬件寄存器时,必须严格遵守硬件手册的规定。

  • 字节序:硬件寄存器通常是大端模式。小端CPU在写入时必须手动转换字节序。
  • 对齐:访问硬件寄存器必须严格对齐。例如,对一个32位(4字节)寄存器,其访问地址必须是4的倍数,否则在很多嵌入式处理器上将导致硬件异常。