结构体对齐和结构体相关宏
1、结构体的对齐访问
1.1、通过示例理解结构体对齐是什么?
(1)结构体中元素的内存对齐示例:
#include <stdio.h>// 定义一个包含不同数据类型成员的结构体
struct MyStruct
{char a; // 1 字节对齐int b; // 4 字节对齐short c; // 2 字节对齐char d; // 1 字节对齐
};int main()
{printf("Size of struct MyStruct: %d bytes\r\n", (int)sizeof(struct MyStruct)); struct MyStruct s;// 输出结构体中各成员的地址,观察其对齐情况printf("Address of a: %p\n", &s.a);printf("Address of b: %p\n", &s.b);printf("Address of c: %p\n", &s.c);printf("Address of d: %p\n", &s.d);return 0;
}
执行结果:
Size of struct MyStruct: 12 bytes
Address of a: 000000000061FE14
Address of b: 000000000061FE18
Address of c: 000000000061FE1C
Address of d: 000000000061FE1E
(2)示例说明
- 结构体
MyStruct
包含了不同数据类型成员,它们具有不同的对齐要求。char
类型通常为 1 字节对齐,int
类型一般为 4 字节对齐,short
类型一般为 2 字节对齐。 - 当编译器为结构体分配内存时,会按照一定的规则(通常是基于系统架构和编译器的对齐策略)进行对齐。
- 例如,在大部分常见的系统中,成员
a
占用 1 字节,之后为了满足int
类型 4 字节对齐的要求,编译器会在a
和b
之间插入 3 个字节的填充,使得b
的地址满足 4 字节对齐的条件。 - 同样地,
b
占用 4 字节后,c
需要满足 2 字节对齐,此时如果b
结束的地址没有满足 2 字节对齐,编译器会在b
和c
之间插入填充字节。 - 最后,
d
后面也可能会根据整个结构体对齐要求进行填充,以确保结构体的整体大小满足最大成员对齐要求,示例中整个结构体的大小可能大于各成员大小之和,这是因为填充字节的存在。
- 例如,在大部分常见的系统中,成员
1.2、结构体为什么需要内存对齐
(1)结构体中元素对齐访问主要原因是为了配合硬件,也就是说硬件本身有物理上的限制,如果对齐访问会提高效率,否则会大大降低效率。
(2)对比对齐访问和不对齐访问:对齐访问牺牲了内存空间,换取了速度性能;而非对齐访问牺牲了访问速度性能,换取了内存空间的完全利用。
1.3、结构体对齐的规则
(1)编译器本身可以设置内存对齐的规则,在32位芯片的编译器中,一般编译器默认对齐方式是4字节对齐。
(2)结构体对齐的关键,编译器为4字节对齐时:
- 结构体整体本身起始位置在4字节对齐处,结构体对齐后的大小必须4的倍数。
- 结构体中每个元素本身都必须对其存放,而每个元素本身都有自己的对齐规则。
- char是1字节对齐
- short是2字节对齐
- int是4字节对齐
- float是4字节对齐
- double是8字节对齐
- 不同数据类型的对齐字节数不同:
- 因为对齐地址可以把相邻的4字节中的数据一次性取出,从而提高访问效率。
- double 在 32 位编译器中是 8 字节对齐,这主要是为了内存访问效率和浮点运算单元的要求,而非仅仅因为 32 位处理器一次读取 4 字节。
- 编译器考虑结构体存放时,以满足以上要求的最少内存需要来排布元素。
1.4、gcc支持但不推荐的对齐指令
(1)对齐指令:
- #pragma pack(n) :从当前位置开始,将当前文件的对齐方式设置为
n
字节对齐,n
的常见取值为 1、2、4、8 等。 - #pragma pack(): 将对齐方式恢复到编译器的默认对齐方式 。
(2)示例代码,设置1字节对齐:
#include <stdio.h>#pragma pack(1)
// 定义一个包含不同数据类型成员的结构体
struct MyStruct
{char a; // 1 字节int b; // 4 字节short c; // 2 字节char d; // 1 字节
};
#pragma pack()int main()
{// Size of struct MyStruct: 8 bytesprintf("Size of struct MyStruct: %d bytes\r\n", (int)sizeof(struct MyStruct));return 0;
}
(3)#pragma是用来指挥编译器,或者说设置编译器的对齐方式的。编译器的默认对齐方式是4,但是有时候我不希望对齐方式是4,而希望是别的(譬如希望1字节对齐,也可能希望是8,甚至可能希望128字节对齐)。
(4)我们需要#prgama pack(n)开头,以#pragma pack()结尾,定义一个区间,这个区间内的对齐字节数就是n。
(5)#prgma pack的方式在很多C环境下都是支持的,但是gcc虽然也可以不过不建议使用。
1.5、gcc推荐的对齐指令
(1)对齐指令:
- __attribute((packed)) / __attribute__((packed))
- 设置为1字节对齐。
- 指定该数据类型(通常是结构体或联合体)的成员以最紧凑的方式存储,即每个成员紧跟在前一个成员之后,不进行任何填充对齐,从而可以尽可能地节省内存空间。
- 作用的范围只有加了这个东西的这一个类型。
__attribute((packed))
和__attribute__((packed))
在功能上是相同的,现代代码中通常使用__attribute__((packed))
这种写法,因为它更符合规范。- 作用于变量无效。
- __attribute__((aligned(n)))
- 用于指定变量、结构体或类型的内存对齐方式。
- __attribute__((aligned(n)))作用于结构体时,作用是让整个结构体变量整体进行n字节对齐,而不是结构体内各元素也要n字节对齐。
- 既可以作用于类型,也可作用于变量。
char c __attribute__((aligned(4))); // 指定变量 c 按 4 字节对齐
(2)__attribute__((packed))使用示例:
#include <stdio.h>// 定义一个包含不同数据类型成员的结构体
struct MyStruct1
{char a; // 1 字节int b; // 4 字节short c; // 2 字节char d; // 1 字节
}__attribute__((packed));struct __attribute__((packed)) MyStruct2
{char a; // 1 字节int b; // 4 字节short c; // 2 字节char d; // 1 字节
};int main()
{// Size of struct MyStruct1: 8 bytesprintf("Size of struct MyStruct1: %d bytes\r\n", (int)sizeof(struct MyStruct1));// Size of struct MyStruct1: 8 bytesprintf("Size of struct MyStruct2: %d bytes\r\n", (int)sizeof(struct MyStruct2));return 0;
}
2、offsetof宏与container_of宏
2.1、offsetof宏
(1)offsetof宏的作用是:用宏来计算结构体中某个元素和结构体首地址的偏移量,其实质是通过编译器来计算的。
(2)使用示例:
#include <stdio.h>struct mystruct
{char a;int b;short c;
};// offsetof宏的原理:TYPE为结构体类型,MEMBER为结构体元素
// ((TYPE *)0): 虚拟一个type类型结构体指针,指向首地址为0的结构体变量。
// 实际上这个结构体变量可能不存在,但是只要我不去解引用这个指针就不会出错。// ((TYPE *)0)->MEMBER:然后用->的方式来访问结构体元素;
// &((TYPE *)0)->MEMBER):再然后通过取地址符获得结构体元素的地址;// 因为虚拟的结构体指针变量指向的结构体的首地址为0,
// 所以获得的 结构体元素的地址 就等效为 结构体元素相对于整个结构体变量首地址的偏移量。
#define offsetof(TYPE, MEMBER) ((int)&((TYPE *)0)->MEMBER)int main(void)
{struct mystruct s1;s1.b =12;// 手工计算结构体元素b的地址int *p1 = (int *)((char *)&s1 + 4);printf("*p1 = %d.\n", *p1);// 通过offsetof宏计算元素b的地址int *p2 = (int *)((int)&s1 + offsetof(struct mystruct, b));printf("*p2 = %d.\n", *p2);// 计算结构体元素相对于整个结构体变量首地址的偏移量int offsetof_a = offsetof(struct mystruct, a);printf("offsetof_a = %d.\n", offsetof_a);int offsetof_b = offsetof(struct mystruct, b);printf("offsetof_b = %d.\n", offsetof_b);int offsetof_c = offsetof(struct mystruct, c);printf("offsetof_c = %d.\n", offsetof_c);return 0;
}
2.2、container_of宏
(1)作用:知道一个结构体中某个元素的指针,反推这个结构体变量的指针。
(2)有了container_of宏,我们可以从一个元素的指针得到整个结构体变量的指针,继而得到结构体中其他元素的指针。
(3)typeof关键字的作用是:由变量名得到变量的数据类型。
int a; typeof(a) b; // 定义了一个和a数据类型相同的变量b。
(4)使用示例
#include <stdio.h>struct mystruct
{char a;int b;short c;
};// 宏说明: TYPE是结构体类型,MEMBER是结构体中一个元素的元素名;
// 宏返回的是MEMBER元素相对于整个结构体变量的首地址的偏移量,类型是int.
#define offsetof(TYPE, MEMBER) ((int)&((TYPE *)0)->MEMBER)// 宏说明:ptr是指向结构体元素member的指针,type是结构体类型,member是结构体中一个元素的元素名;
// 宏返回的就是指向结构体变量的指针,类型是(type *).
#define container_of(ptr, type, member) ({ \const typeof(((type *)0)->member) * __mptr = (ptr); \(type *)((char *)__mptr - offsetof(type, member)); })int main(void)
{struct mystruct s1;struct mystruct *pS = NULL;short *p = &s1.c; // 指向结构体中成员c的指针// 一般情况获取结构体变量的地址printf("s1的地址等于:%p.\n", &s1);// 已知结构体成员的指针,计算结构体变量的指针。pS = container_of(p, struct mystruct, c);printf("pS等于:%p.\n", pS);return 0;
}
(5)宏原理说明:
// 宏说明: TYPE是结构体类型,MEMBER是结构体中一个元素的元素名;
// 宏返回的是MEMBER元素相对于整个结构体变量的首地址的偏移量,类型是int.
#define offsetof(TYPE, MEMBER) ((int)&((TYPE *)0)->MEMBER)// 宏说明:ptr是指向结构体元素member的指针,type是结构体类型,member是结构体中一个元素的元素名;
// 宏返回的就是指向结构体变量的指针,类型是(type *).
#define container_of(ptr, type, member) ({ \const typeof(((type *)0)->member) * __mptr = (ptr); \(type *)((char *)__mptr - offsetof(type, member)); })
- ((type *)0): 虚拟一个type类型结构体指针,指向首地址为0的结构体变量。
- (((type *)0)->member): 通过->访问结构体成员member。
- typeof(((type *)0)->member): 通过tyeof得到结构体成员member的数据类型。
- const typeof(((type *)0)->member) * __mptr: 定义指向member类型的指针变量__member。const表示指针指向的内容不能被修改,因为是虚拟的结构体变量,解引用会内存非法访问。
- const typeof(((type *)0)->member) * __mptr = (ptr); :结构体成员变量指针赋值。
- (type *)((char *)__mptr - offsetof(type, member)); :结构体成员变量的地址 减去 该成员相对于整个结构体变量的首地址的偏移量,得到结构体的地址。