一、结构体的对齐访问
总体上遵循两个原则:
(1)、整体空间是占用空间最大的成员(的类型)所占字节数的整数倍;
(2)、数据对齐原则---内存按结构体成员的先后顺序排列,当排到该成员时,其前面已摆放的空间大小必须是该成员类型大小的整数倍,如果不够则补齐,依次向后类推。(变量声明的顺序也可能影响内存的分配方式)
我们举个栗子: 某台机器是32位,即这个机器上char 占1个字节,int占4个字节,double占8字节。
struct A
{
char a;
double b;
int c;
char d;
};
那么,这个结构体占用多少字节呢?
<1>、它的第一个成员是char 类型的 a,占 1 个字节。放入结构的0地址处:
<2>、下来要存储的是第二个成员是double 类型的 b,占 8 个字节。它该怎么存放呢?
我们回想到原则(2)中说,存储到某个成员时,前面已经存放的所有成员所占的总空间大小是改成员大小的整数倍,不够则补齐!
也就是<1>图中显示的下一个存储成员起始地址应该是 8 的整数倍,而上图中显示的起始地址为 1 ,并不是 8 的整数倍,所以需要补齐。向后增加地址数到 8 (补齐操作),这时 8 是 8 的整数倍,可以存储。则将成员 b 存放到 8 地址处:
<3>、下来要存储的是第三个成员是 int 类型的 c,占 4 个字节,由上图所知,下一个存储成员的起始地址是16,刚好是 4 的倍数。所以不用补齐,直接用 4 个字节来存储成员 c:
<4>、最后存储的是第四个成员是 char 类型的 d,占 1 个字节。由上图所知,下一个存储成员的起始地址是20,刚好是 1 的倍数。所以不用补齐,直接用 1 个字节来存储 成员 d:
<5>、把所有成员都存储到位,这就完了吗?
No!我们还要考虑最后一个要素:原则(1),整体空间是占用空间最大的成员(的类型)所占字节数的整数倍!而这个结构的占用空间最大的成员是 double 类型的 b,占用 8 个字节。而现在整体空间由上图所知是 21 个字节,并不是 8 的倍数,所以仍需要补齐!补多少呢? 比 21 大的最小的 8 的倍数是 24,所以就补到 24 字节:
所以最终算下来,结构体 A 的大小是 24字节!
最后,我们再来用实际代码测试一下:
#include <stdio.h>
struct A
{
char a;
double b;
int c;
char d;
};
int main(void)
{
printf("sizeof(char) = %d\n", sizeof(char));
printf("sizeof(int) = %d\n", sizeof(int));
printf("sizeof(double) = %d\n\n", sizeof(double));
printf("sizeof(struct A) = %d\n", sizeof(struct A));
return 0;
}
运行截图:
二、offsetof宏与container_of宏
两个宏都在stddef.h头文件中定义
1、offsetof宏:
offsetof(TYPE, MEMBER) ((size_t)&((TYPE*)0)->MEMBER)
(1)offsetof宏的作用是:用宏来计算结构体中某个元素和结构体首地址的偏移量(其实质是通过编译器来帮我们计算)。
TYPE是结构体类型,MEMBER是结构体中一个元素的元素名
这个宏返回的是member元素相对于整个结构体变量的首地址的偏移量,类型是size_t
(2)解析:
(TYPE *)0 这是一个强制类型转换,把0地址强制类型转换成一个指针,这个指针指向一个TYPE类型的结构体变量。 (实际上这个结构体变量可能不存在,但是只要我不去解引用这个指针就不会出错)。
((TYPE *)0)->MEMBER ,(TYPE *)0是一个TYPE类型结构体变量的指针,通过指针指针来访问这个结构体变量的member元素
&((TYPE *)0)->MEMBER 等效于&(((TYPE *)0)->MEMBER),因为指针运算符的优先级高于取地址的优先级。意义就是得到member元素的地址。但是因为整个结构体变量的首地址是0,所以整个宏的作用就是得到member的相对偏移地址。
#include <stdio.h>
#include <stddef.h>
struct mystruct
{
char a; // 偏移0
int b; // 偏移4
short c; // 偏移8
};
int main(void)
{
struct mystruct s1;
s1.b = 12;
int offsetofa = offsetof(struct mystruct, a);
printf("offsetofa = %d.\n", offsetofa);
int offsetofb = offsetof(struct mystruct, b);
printf("offsetofb = %d.\n", offsetofb);
int offsetofc = offsetof(struct mystruct, c);
printf("offsetofc = %d.\n", offsetofc);
printf("整个结构体变量的首地址:%p.\n", &s1);
printf("s1.b的首地址:%p.\n", &(s1.b));
printf("偏移量是:%d.\n", (char *)&(s1.b) - (char *)&s1);
return 0;
}
2、container_of宏:
#define container_of(ptr, type, member) ({ \
const typeof(((type*)0)->member)* __mptr = (ptr); \
(type*)((char*))__mptr - offsetof(type, member); })
({ }),它是 GNU C 编译器的语法扩展,它与逗号表达式的作用类似,结果为最后一个语句的值
typeof关键字的作用是:typepef(a)时由变量a得到a的类型,typeof就是由变量名得到变量数据类型的。
(1)作用:已知结构体type的成员member的地址ptr,求解结构体type的起始地址。ptr是指向结构体元素member的指针,type是结构体类型,member是结构体中一个元素的元素名这个宏返回的就是指向整个结构体变量的指针,类型是(type *)。有了container_of宏,我们可以从一个元素的指针得到整个结构体变量的指针,继而得到结构体中其他元素的指针。
(2)工作原理:
先用typeof得到member元素的类型定义成一个指针(__mptr),const typeof( ((type *)0)->member ) *__mptr = (ptr);
然后用这个指针减去该元素相对于整个结构体变量的偏移量(偏移量用offsetof宏得到的),减去之后得到的就是整个结构体变量的首地址了,( (char *)__mptr - offsetof(type,member) ),把mptr指针强转成(char *)是因为,char指针减法只移一个字节,如果这样才可能得出准确的地址,否则,改为int类型,在减1就移动4个就乱了。
再把这个地址强制类型转换为type *即可, (type*)((char*))__mptr - offsetof(type, member);
(3)实例代码:
#include <stdio.h>
#include <stddef.h>
struct mystruct
{
char a; // 偏移0
int b; // 偏移4
short c; // 偏移8
};
int main(void)
{
struct mystruct s1;
struct mystruct *pS = NULL;
short *p = &(s1.c); // p就是指向结构体中某个member的指针
printf("s1的指针等于:%p.\n", &s1);
// 问题是要通过p来计算得到s1的指针
pS = container_of(p, struct mystruct, c);
printf("pS等于:%p.\n", pS);
return 0;
}
REF: