一、结构体对齐#
1. 默认对齐规则#
1.1. 规则#
数据类型自身的对齐值
int8_t / uint8_t 型:1 字节。
int16_t / uint16_t 型:2 字节。
int32_t / uint32_t / float 型:4 字节。
double 型:8 字节。
结构体的自身对齐值
其成员中自身对齐值最大的那个值。
数据成员、结构体的有效对齐值
自身对齐值和指定对齐值中较小者,即 有效对齐值 = 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) 的方式对齐。
结构体中第一个成员放在 offset 为 0 的地方,接下来每个成员的对齐,使用的是 n 和自身长度中较小的那个值。
假设设置的是 4 字节对齐,但是成员的自身长度是 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
|
- a 的长度是 1,n = 1,取其中较小那个,也就是 1。
- b 的长度是 2,n = 1,取其中较小那个,也就是 1。
- c 的长度是 2,n = 1,取其中较小那个,也就是 1。
- d 的长度是 4,n = 1,取其中较小那个,也就是 1。
- 结构体中最大成员的长度是 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
|
- a 的长度是 1,n = 8,取其中较小那个,也就是 1。
- b 的长度是 2,n = 8,取其中较小那个,也就是 2。
- c 的长度是 2,n = 8,取其中较小那个,也就是 2。
- d 的长度是 4,n = 8,取其中较小那个,也就是 4。
- 结构体中最大成员的长度是 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. 单字节大小的联合体#
如果结构体使用位域的话,需要注意大小端。
具体取决于编译器。
代码测试:
测试环境: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;
}
|
我们期望的结果是这样的:
- head = 0b111,即 7。
- body = 0b1001,即 9。
- end = 0b1,即 1。
实际结果是:
1
2
3
4
5
6
| $ gcc -o main main.c
$ ./main
byte: 243
head: 3
body: 14
end: 1
|
也就是说,由于 GCC 把先声明的位于放到低位,所以:
- head = 0b011,即 3。
- body = 0b1110,即 14。
- 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]));
}
|