1. 看一段代码,回答一个问题:
#include <stdio.h>
int main() {
typedef struct Person{
char *name;
int age;
char *id;
}Person;
printf("sizeof(Person): %d\n", sizeof(Person)); // sizeof(Person): 24
printf("sizeof(char *): %d\n", sizeof(char *)); // sizeof(char *): 8
printf("sizeof(int): %d\n", sizeof(int)); // sizeof(int): 4
return 0;
}
结构体内部三个类型变量,每个类型变量对应的字节数是 8,4,8,共占据 20 个字节,那为什么该结构体占据的字节数是 24?这便是结构体对齐的由来
2. 结构体对齐,结构体内部的每个类型对应的地址是其类型大小的倍数,比如 char * 对应的地址一定是 sizeof(char *) 的倍数,举一个 demo
typedef struct {
char a; // 1
char b; // 1
int c; // 4
short d; // 2
double e; // 8
}Align;
Align align = {.a='a', .b='b', .c=3, .d=4, .e=5};
printf("sizeof(align): %d\n", sizeof(align)); // sizeof(align): 24
可以看出,a b 存放在一起,之后空两个字节,在 12 的位置上存储了 c,在 0 的位置上存储了 d,然后空了 6 个字节,将 e 存储在地址倍数为 8 的地方(结构体内部的每个类型对应的地址是其类型大小的倍数),所以不合理的结构体会造成空间的浪费,24 个字节中 只有 16 个字节是真正存储东西的,浪费了 8个字节,就是阴影部分的 cc
将结构体内部的属性稍微改变顺序:
typedef struct {
char a; // 1
char b; // 1
short d; // 2
int c; // 4
double e; // 8
}Align;
Align align = {.a='a', .b='b', .c=3, .d=4, .e=5};
printf("sizeof(align): %d\n", sizeof(align)); // 16
3. 除了改变结构体内部的属性顺序,编译器也允许改变结构体储存方式,不会让结构体按照上面的方式进行对齐
#pragma pack(2) // 编译器默认以 2 的倍数进行对齐,不再按照类型的大小
typedef struct {
char a; // 1
char b; // 1
int c; // 4
short d; // 2
double e; // 8
}Align;
Align align = {.a='a', .b='b', .c=3, .d=4, .e=5};
本来是占据 24 字节,采用对齐方式后,仅占据 16个字节,int c 的地址是从 11 开始的,11-14
、
4. 如果是 gcc 编译器,有一个内置的函数,可以改变结构体内部单个属性的对齐方式(暂且试验失败),试验了其他博客,也失败,推测是 gcc 编译器版本问题。https://blog.csdn.net/21aspnet/article/details/6729724 这篇博客讲述了为什么要字节对齐,可以看看内部的原理。(字节对齐是为了 CPU 访问效率更高)
5. C11 提供了一个新的特性,也是改变对齐方式的:_Alignas(8) int c; 只不过还是仅 gcc 编译器可用,msvc 编译器不通过;此外,该特性对齐方式的大小有要求,最小是对应数据类型的大小,比如 _Alignas(n) int c,n 至少为 4,不能取 4 以下的数,换成 double 则 n 至少为 8
typedef struct {
char a; // 1
char b; // 1
_Alignas(8) int c; // 4
short d; // 2
double e; // 8
}Align;
Align align = {.a='a', .b='b', .c=3, .d=4, .e=5};
printf("sizeof(align): %d\n", sizeof(align)); // sizeof(align): 24
6. 介绍一个 C11 的新东西,_Alignof() 查询一个字段对齐的倍数,比如刚才的 int c,由于前面添加了 _Alignas(8),所以 _Alignof(align.c) 的结果就是 8,这个东西也是仅 gcc 可用,msvc 不可用
7. 介绍一个 msvc 和 gcc 都可用的 offsetof(Align, e),第一个参数是结构体类型,第二个参数是结构体内部的属性,整体的意思是该属性偏移结构体初始位置多少个字节
#include <stdio.h>
#include <stddef.h>
int main() {
typedef struct {
char a; // 1
char b; // 1
int c; // 4
short d; // 2
double e; // 8
}Align;
Align align = {.a='a', .b='b', .c=3, .d=4, .e=5};
printf("%d", offsetof(Align, e)); // 16
return 0;
}
深究一下 offsetof,可以看到(gcc下面看不到,msvc可以看到)
首先注意,s 是传进来的结构体类型,m 是传进来的结构体成员,(s *)0 是骗编译器说有一个指向类(或结构)s 的指针,该指针的值为 0,((s *)0)->m 是利用指针指向结构体的成员 m,&((s *)0)->m 是要取得类 s 中成员变量 m 的地址。由于这个类(或结构)的基址为 0(因为指针的值为 0),这时 m 的地址当然就是 m 在 s 中的偏移了。
(s *)0 是把 0 地址转换为 s 指针类型,类比于 (char*) a 这种感觉。恰好这时的 0 是 s 类型的,因此可以取成员变量。从这个指针上“取” m 成员再取址,而 m 成员的地址转换后结果就是 m 成员相对于整个对象的偏移量(既然是从 0 地址开始算的,就不用再减去起始地址0)。