一、结构体对齐

1. 默认对齐规则

1.1. 规则

  1. 数据类型自身的对齐值

    int8_t / uint8_t 型:1 字节。

    int16_t / uint16_t 型:2 字节。

    int32_t / uint32_t / float 型:4 字节。

    double 型:8 字节。

  2. 结构体的自身对齐值

    其成员中自身对齐值最大的那个值。

  3. 数据成员、结构体的有效对齐值

    自身对齐值和指定对齐值中较小者,即 有效对齐值 = min(自身对齐值,当前指定的对齐值)

1.2. 示例

假如结构体的声明如下:

1
2
3
4
5
6
typedef struct {
    uint8_t  a;
    uint16_t b;
    uint16_t c;
    uint32_t d;
} struct_demo_t;

假设当前默认是 4 字节对齐,该结构体的大小是 12 Bytes,内存的使用空间如下:

1
2
3
4
5
6
7
+---+---+---+---+
| a |   |   b   |
+---+---+---+---+
|   c   |       |
+---+---+---+---+
|       d       |
+---+---+---+---+

代码测试(main.c):

测试环境:Windows 10 + MSYS2 + GCC 13.3.0

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
#include <stdio.h>
#include <stdint.h>
#include <string.h>

typedef struct {
    uint8_t  a;
    uint16_t b;
    uint16_t c;
    uint32_t d;
} struct_demo_t;

int main(int argc, char *argv[])
{
    uint8_t *ptr = NULL;
    size_t size = 0;
    struct_demo_t demo;
    
    memset(&demo, 0, sizeof(demo));

    /* 打印结构体大小 */
    size = sizeof(struct_demo_t);
    printf("size: %d Bytes\r\n", size);

    ptr = (uint8_t *)&demo;

    /* 打印结构体的内存信息 */
    printf("old:\r\n");
    for (size_t i = 0; i < size; i++) {
        if ((i != 0) && ((i % 4) == 0)) {
            printf("\r\n");
        }
        printf("0x%02x ", ptr[i]);
    }
    printf("\r\n");

    demo.a = 0x01;
    demo.b = 0x1234;
    demo.c = 0x5678;
    demo.d = 0xaabbccdd;

    /* 赋值后, 打印结构体的内存信息 */
    printf("new:\r\n");
    for (size_t i = 0; i < size; i++) {
        if ((i != 0) && ((i % 4) == 0)) {
            printf("\r\n");
        }
        printf("0x%02x ", ptr[i]);
    }
    printf("\r\n");

    return 0;
}

编译和执行结果:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
$ gcc -o main main.c
$ ./main
size: 12 Bytes
old:
0x00 0x00 0x00 0x00
0x00 0x00 0x00 0x00
0x00 0x00 0x00 0x00
new:
0x01 0x00 0x34 0x12
0x78 0x56 0x00 0x00
0xdd 0xcc 0xbb 0xaa

可以发现,打印出来的结果和上面的是一致的。

后面再说数据为什么是 0x34 0x12 而不是 0x12 0x34。

2. 更改对齐规则一:#pragma pack(n)

2.1. 说明

用于限制结构体、联合体成员的最大对齐边界,即强制结构体内部成员按照 min(自然对齐, n) 的方式对齐。

  1. 结构体中第一个成员放在 offset 为 0 的地方,接下来每个成员的对齐,使用的是 n 和自身长度中较小的那个值。

    假设设置的是 4 字节对齐,但是成员的自身长度是 2 字节,那么该成员的对齐就是 2 字节对齐

  2. 结构体中的所有成员对齐完成后,结构体本身也要对齐,使用的是 n 和结构体中最大的成员长度中较小的那个值。

    假设设置的是 8 字节对齐,但是结构体中最长的成员长度是 4 字节,那么这个结构体还是使用 4 字节对齐

2.2. 常见用法

1
2
3
4
5
6
7
8
/* 获取编译器当前的对齐方式 */
#pragma pack(show)

/* 设置对齐的方式为 n 字节对齐, 这个 n 的常用的取值有: 1, 2, 4, 8, 16 */
#pragma pack(n)

/* 取消自定义的对齐方式, 恢复为默认 */
#pragma pack()

用法:

1
2
3
4
5
#pragma pack(1)
struct {
    ...
};
#pragma pack()

2.3. 示例一

1
2
3
4
5
6
7
8
#pragma pack(1)
typedef struct {
    uint8_t  a;
    uint16_t b;
    uint16_t c;
    uint32_t d;
} struct_demo_t;
#pragma pack()

该结构体的大小是 9 Bytes,内存的使用空间如下:

1
2
3
4
5
6
7
+---+---+---+---+
| a |   b   | c |
+---+---+---+---+
| c |     d     |
+---+---+---+---+
| d |          
+---+

使用相同的代码进行测试,编译和执行结果:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
$ gcc -o main main.c
$ ./main
size: 9 Bytes
old:
0x00 0x00 0x00 0x00
0x00 0x00 0x00 0x00
0x00
new:
0x01 0x34 0x12 0x78
0x56 0xdd 0xcc 0xbb
0xaa
  1. a 的长度是 1,n = 1,取其中较小那个,也就是 1。
  2. b 的长度是 2,n = 1,取其中较小那个,也就是 1。
  3. c 的长度是 2,n = 1,取其中较小那个,也就是 1。
  4. d 的长度是 4,n = 1,取其中较小那个,也就是 1。
  5. 结构体中最大成员的长度是 4,n = 1,取其中较小那个,也就是 1。

2.4. 示例二

1
2
3
4
5
6
7
8
#pragma pack(8)
typedef struct {
    uint8_t  a;
    uint16_t b;
    uint16_t c;
    uint32_t d;
} struct_demo_t;
#pragma pack()

该结构体的大小是 12 Bytes,内存的使用空间如下:

1
2
3
4
5
6
7
+---+---+---+---+
| a |   |   b   |
+---+---+---+---+
|   c   |       |
+---+---+---+---+
|       d       |
+---+---+---+---+

使用相同的代码进行测试,编译和执行结果:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
$ gcc -o main main.c
$ ./main
size: 12 Bytes
old:
0x00 0x00 0x00 0x00
0x00 0x00 0x00 0x00
0x00 0x00 0x00 0x00
new:
0x01 0x00 0x34 0x12
0x78 0x56 0x00 0x00
0xdd 0xcc 0xbb 0xaa
  1. a 的长度是 1,n = 8,取其中较小那个,也就是 1。
  2. b 的长度是 2,n = 8,取其中较小那个,也就是 2。
  3. c 的长度是 2,n = 8,取其中较小那个,也就是 2。
  4. d 的长度是 4,n = 8,取其中较小那个,也就是 4。
  5. 结构体中最大成员的长度是 4,n = 8,取其中较小那个,也就是 4。

2.5. 示例三

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
#pragma pack(1)
typedef struct {
    uint8_t  a;
    uint16_t b;
    uint16_t c;
    uint32_t d;
} struct_demo_item_t;
#pragma pack()

#pragma pack(8)
typedef struct {
    uint8_t  a;
    uint16_t b;
    uint16_t c;
    uint32_t d;
    struct_demo_item_t demo;
} struct_demo_t;
#pragma pack()

这个结构体的大小是 24 字节,内存的使用空间如下:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
+---+---+---+---+
| a |   |   b   |
+---+---+---+---+
|   c   |       |
+---+---+---+---+
|       d       |
+---+---+---+---+
| a |   b   | c |
+---+---+---+---+
| c |     d     |
+---+---+---+---+
| d |           |
+---+-----------+

测试代码的赋值为:

1
2
3
4
5
6
7
8
9
    demo.a = 0x01;
    demo.b = 0x1234;
    demo.c = 0x5678;
    demo.d = 0xaabbccdd;

    demo.demo.a = 0x10;
    demo.demo.b = 0x4321;
    demo.demo.c = 0x8765;
    demo.demo.d = 0xddccbbaa;

使用相同的代码进行测试,编译和执行结果:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
$ gcc -o main main.c
$ ./main
size: 24 Bytes
old:
0x00 0x00 0x00 0x00
0x00 0x00 0x00 0x00
0x00 0x00 0x00 0x00
0x00 0x00 0x00 0x00
0x00 0x00 0x00 0x00
0x00 0x00 0x00 0x00
new:
0x01 0x00 0x34 0x12
0x78 0x56 0x00 0x00
0xdd 0xcc 0xbb 0xaa
0x10 0x21 0x43 0x65
0x87 0xaa 0xbb 0xcc
0xdd 0x00 0x00 0x00

2.6. 示例四

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
typedef struct {
    uint8_t  a;
    uint16_t b;
    uint16_t c;
    uint32_t d;
} struct_demo_item_t;

#pragma pack(8)
typedef struct {
    uint8_t  a;
    uint16_t b;
    uint16_t c;
    uint32_t d;
    struct_demo_item_t demo;
} struct_demo_t;
#pragma pack()

这个结构体的大小是 24 字节,内存的使用空间如下:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
+---+---+---+---+
| a |   |   b   |
+---+---+---+---+
|   c   |       |
+---+---+---+---+
|       d       |
+---+---+---+---+
| a |   |   b   |
+---+---+---+---+
|   c   |       |
+---+---+---+---+
|       d       |
+---+---+---+---+

测试代码的赋值为:

1
2
3
4
5
6
7
8
9
    demo.a = 0x01;
    demo.b = 0x1234;
    demo.c = 0x5678;
    demo.d = 0xaabbccdd;

    demo.demo.a = 0x10;
    demo.demo.b = 0x4321;
    demo.demo.c = 0x8765;
    demo.demo.d = 0xddccbbaa;

使用相同的代码进行测试,编译和执行结果:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
$ gcc -o main main.c
$ ./main
size: 24 Bytes
old:
0x00 0x00 0x00 0x00
0x00 0x00 0x00 0x00
0x00 0x00 0x00 0x00
0x00 0x00 0x00 0x00
0x00 0x00 0x00 0x00
0x00 0x00 0x00 0x00
new:
0x01 0x00 0x34 0x12
0x78 0x56 0x00 0x00
0xdd 0xcc 0xbb 0xaa
0x10 0x00 0x21 0x43
0x65 0x87 0x00 0x00
0xaa 0xbb 0xcc 0xdd

3. 更改对齐规则二:__attribute__((__aligned(n)))

3.1. 说明

__attribute__((__aligned(n))) 通常用于指定某个特定变量或类型的最小对齐字节数,也就是说,该变量或类型的起始地址将是 n 的倍数。

n 必须是 2 的幂。

该指令是 GCC 编译器特有的属性语法(当前 Clang 已支持)。

3.2. 常见用法

1
2
/* 设置对齐的方式为 n 字节对齐, 这个 n 的常用的取值有: 1, 2, 4, 8, 16 */
__attribute__((aligned(n)))

用法:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
typedef struct {
    ...
} __attribute__((aligned(4))) demo_1;

struct __attribute__((aligned(4))) demo_2 {
    ...
};

struct demo_3 {
    ...
    uint32_t param __attribute__((aligned(8)));
};

uint16_t var1 __attribute__((aligned(8))) = 0;

uint16_t array[3] __attribute__ ((aligned(8)));

3.3. 示例一

1
2
3
4
5
6
typedef struct {
    uint8_t  a;
    uint16_t b;
    uint16_t c;
    uint32_t d;
} __attribute__((__aligned__(8))) struct_demo_t;

这个结构体的大小是 16 字节,内存的使用空间如下:

1
2
3
4
5
6
7
8
9
+---+---+---+---+
| a |           |
+---+---+---+---+
|   b   |   c   |
+---+---+---+---+
|       d       |
+---+---+---+---+
|               |
+---+---+---+---+

使用相同的代码进行测试,编译和执行结果:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
$ gcc -o main main.c
$ ./main
old:
0x00 0x00 0x00 0x00
0x00 0x00 0x00 0x00
0x00 0x00 0x00 0x00
0x00 0x00 0x00 0x00
new:
0x01 0x00 0x34 0x12
0x78 0x56 0x00 0x00
0xdd 0xcc 0xbb 0xaa
0x00 0x00 0x00 0x00

结构体的大小是 12 字节,但是要 8 字节对齐,所以实际上 16 字节,使用 4 字节的填充。

二、结构体的大小端

1. 大小端的说明

  • 大端(Big-Endian)

    高位字节存放在低地址,低位字节存放在高地址。

     1
     2
     3
     4
     5
     6
     7
     8
     9
    10
    11
    
    /**
     * 将数值 0x12345678 放入数组
     * (从高位到地位分别是 0x12 0x34 0x56 0x78)
     */
    
    uint8_t buf[] = {0x12, 0x34, 0x56, 0x78};
    
    /**
     * 此时 buf[0] = 0x12, 刚好满足: 高位字节存放在低地址
     * 此时 buf[3] = 0x78, 刚好满足: 低位字节存放在高地址
     */
    
  • 小端(Little-Endian)

    低位字节存放在低地址,高位字节存放在高地址。

     1
     2
     3
     4
     5
     6
     7
     8
     9
    10
    11
    
    /**
     * 将数值 0x12345678 放入数组
     * (从高位到地位分别是 0x12 0x34 0x56 0x78)
     */
    
    uint8_t buf[] = {0x78, 0x56, 0x34, 0x12};
    
    /**
     * 此时 buf[0] = 0x78, 刚好满足: 低位字节存放在低地址
     * 此时 buf[3] = 0x12, 刚好满足: 高位字节存放在高地址
     */
    

一般来说,x86 / x86-64、ARM 都是使用小端,PowerPC、网络通信都是使用大端。

2. 单字节大小的联合体

如果结构体使用位域的话,需要注意大小端。

  • 在小端架构上:

    GCC/Clang 通常将结构体里面先声明的位域放在低位

  • 在大端架构上:

    结构体里面先声明的位域可能放在高位。

具体取决于编译器。

代码测试:

测试环境:Windows 10 + MSYS2 + GCC 13.3.0

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
#include <stdio.h>
#include <stdint.h>

typedef union {
    uint8_t byte;
    struct {
        uint8_t head : 3;
        uint8_t body : 4;
        uint8_t end  : 1;
    } data;
} demo_t;

int main(int argc, char *argv[])
{
    demo_t demo;

    demo.byte = 0b11110011;

    printf("byte: %d\r\n", demo.byte);

    printf("head: %d\r\n", demo.data.head);
    printf("body: %d\r\n", demo.data.body);
    printf("end: %d\r\n",  demo.data.end);

    return 0;
}

我们期望的结果是这样的:

  1. head = 0b111,即 7。
  2. body = 0b1001,即 9。
  3. end = 0b1,即 1。

实际结果是:

1
2
3
4
5
6
$ gcc -o main main.c
$ ./main
byte: 243
head: 3
body: 14
end: 1

也就是说,由于 GCC 把先声明的位于放到低位,所以:

  1. head = 0b011,即 3。
  2. body = 0b1110,即 14。
  3. end = 0b1,即 1。

为了解决需求,我们需要修改结构体:

1
2
3
4
5
6
7
8
typedef union {
    uint8_t byte;
    struct {
        uint8_t end  : 1;
        uint8_t body : 4;
        uint8_t head : 3;
    } data;
} demo_t;

这时候打印的结果才是我们期望的:

1
2
3
4
5
6
$ gcc -o main main.c
$ ./main
byte: 243
head: 7
body: 9
end: 1

三、结构体和联合体

1. 通信协议

假如通信协议如下:

1
2
3
4
Byte    3         4          2       1      54
    +--------+----------+---------+-----+---------+
    | header | magic id | version | pad | payload |
    +--------+----------+---------+-----+---------+

那么我们可以这样定义结构体:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
#pragma pack(1)
typedef union {
    uint8_t data[64];
    struct {
        uint8_t header[3];
        uint32_t magic_id;
        uint16_t version;
        uint8_t pad;
        uint8_t payload[54];
    } frame;
} frame_t;
#pragma pack()

那么我们就可以这样使用:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
frame_t frame;

/* 单独赋值 */
frame.frame.header[0] = 0x11;
frame.frame.header[1] = 0x22;
frame.frame.header[2] = 0x33;

frame.frame.magic_id = 0xaabbccdd;

/* 操作缓冲区 (注意溢出) */
uint8_t src_data[4] = {0x12, 0x00, 0x00, 0x00};
memcpy(frame.frame.data, src_data, 4);

/* 操作数据完成后, 保存到发送缓冲区 */
uint8_t *send_buf = (uint8_t *)malloc(1024);
if (send_buf != NULL) {
    memcpy(send_buf, frame.data, sizeof(frame.data)/sizeof(frame.data[0]));
}

版权声明

本文为「Zeepunt 日常随笔」的原创文章,遵循 CC 4.0 BY-SA 版权协议。

原文链接:https://zeepunt.github.io/article/c/c%E7%BB%93%E6%9E%84%E4%BD%93%E7%9A%84%E4%BD%BF%E7%94%A8/