昨天分享了结构体里面的一些常见用法(因为测试代码测试的有点晚,有些地方没有分享完。),今天我们来继续分享结构体里面的其他用法。
一、结构体对齐问题:
1、在讨论这个问题之前,我们先来看一个代码示例:
代码语言:javascript复制 #include <stdio.h>
struct A{
};
int main(void)
{
printf("the struct A is %dn",sizeof(struct A));
return 0;
}
演示结果:
在gcc编译环境演示结果:
说明:
从这个试验现象当中,我们可以看出,在结构体初始化的时候,里面什么类型的数据都没有定义的时候,结构体所占用的内存字节大小为0(按照常规思路来想很正常,因为结构体本身就是我们自己定义的一种复杂数据类型,所谓复杂数据就是里面聚集各种基本数据类型,比如int , float char 等;不过这里这个现象我之前有网友讨论过,以为是对的,但是之后又有网友指出,在vc 6.0里面,这样定义却占用一个字节大小的,看到这里我只能说,c的不同标准要求不一样或者编译器不同吧,为此我特地下了一个vc 6.0编译器来做了一下试验,结果还真是一个字节,不过你要注意的是它是c 后缀名,但是你在vc 6.0(还有vs,这两个编译里不能申明一个空的结构体,必须要有一个结构体成员来才行)写成c语言程序空结构体的话,它会报错,在新一点的编译器里面就不会报错(比如dev,gcc)。为了搞清楚这个,我特地把上面的那个那个试验文件改成c 的源文件,它打印出来的也是1个字节,这个真的要注意哦!),下面是试验现象:
代码语言:javascript复制#include <stdio.h>
struct student
{
};
int main(void)
{
printf("the struct A is %dn",sizeof(struct student));
return 0;
}
试验结果:
说明(这里是c 里才这样,在c语言里输出的结果不一样的):
2、结构体对齐问题:
我们还是先慢慢来引导出这个问题,为此我们先来一个例子:
代码语言:javascript复制 #include <stdio.h>
struct A{
int a;
char b;
float c;
};
int main(void)
{
printf("the int is %dn",sizeof(int));
printf("the char is %dn",sizeof(char));
printf("the float is %dn",sizeof(float));
printf("the struct A is %dn",sizeof(struct A));
return 0;
}
演示输出结果:
说明:
你会很惊讶,怎么结构体所占用的内存字节大小变成了12个字节,那个int 为4个字节,char 为1个字节,float为4个字节,按道理说,加起来是9个字节才对啊(怎么sizeof出来12个字节了)。这个就是我们接下来要讨论的结构体对齐问题了。
3、下面我们就来接着分析上面最后打印出结构体占用内存大小为12个字节,却不是9个字节大小的原因。首先我们要搞清楚好端端的结构体为啥要字节对齐呢?在这之前,我们先来了解一下字节对齐概念:
a、什么是字节对齐:
在C语言中,结构体是一种复合数据类型,其构成元素既可以是基本数据类型(如int、long、float等)的变量,也可以是一些复合数据类型(如数组、结构、联合等)的数据单元(我上面有介绍)。在结构中,编译器为结构体的每个成员按其自然边界(alignment)分配空间。各个成员按照它们被声明的顺序在内存中顺序存储,第一个成员的地址和整个结构的地址相同。为了使CPU能够对变量进行快速的访问,变量的起始地址应该具有某些特性,即所谓的”对齐”. 比如4字节的int型,其起始地址应该位于4字节的边界上,即起始地址能够被4整除。
b、为啥要字节对其呢,主要有以下几种原因:
(1)结构体中元素对齐访问主要原因是为了配合硬件,也就是说硬件本身有物理上的限制,如果对齐排布和访问会提高效率,否则会大大降低效率。
(2)内存本身是一个物理器件(DDR内存芯片,SoC上的DDR控制器),本身有一定的局限性:如果内存每次访问时按照4字节对齐访问,那么效率是最高的;如果你不对齐访问效率要低很多。
(3)还有很多别的因素和原因,导致我们需要对齐访问。譬如Cache的一些缓存特性,还有其他硬件(譬如MMU、LCD显示器)的一些内存依赖特性,所以会要求内存对齐访问。
(4)对比对齐访问和不对齐访问:对齐访问牺牲了内存空间,换取了速度性能;而非对齐访问牺牲了访问速度性能,换取了内存空间的完全利用。
- 小结:说白了,就是为访问结构体成员效率高,也就是读取数据更为高效(但是这里也会牺牲一点点内存)。
c、我们还是用上面的那个例子来分析
代码语言:javascript复制 #include <stdio.h>
struct A{
int a;
char b;
float c;
};
int main(void)
{
printf("the int is %dn",sizeof(int));//4
printf("the char is %dn",sizeof(char));//1
printf("the float is %dn",sizeof(float));//4
printf("the struct A is %dn",sizeof(struct A));//12
return 0;
}
分析过程:
首先是整个结构体,整个结构体变量4字节对齐是由编译器保证的,我们不用操心。然后是第一个元素a,a的开始地址就是整个结构体的开始地址,所以自然是4字节对齐的。但是a 的结束地址要由下一个元素说了算。然后是第二个元素b,因为上一个元素a本身占4字节,本身就是对齐的。所以留给b的开始地址也是 4字节对齐地址,所以b可以直接放(b放的位置就决定了a一共占4字节,因为不需要填充)。b的起始地址定了后,结束地址不能定(因为可能需要填充,所谓填充就是我上面说的要浪费一点内存了,但是没关系啦,我们提高了访问速度,不用多次去访问。),结束地址要看下一个元素来定。然后是第三个元素c,float类型需要4字节对齐(float类型元素必须放在类似0,2,4,8这样的 地址处,不能放在1,3这样的奇数地址处),因此c不能紧挨着b来存放,解决方案是在b之后添加3字节的填充(padding),然后再开始放c。c放完之后还没结束 当整个结构体的所有元素都对齐存放后,还没结束,因为整个结构体大小还要是4的整数倍,这一点非常重要,所以最后就是12个字节啦。下面是我用示意头图来展示:
这里我还要说明一下,有人可能有这样的疑惑,b加一个字节不就是2个字节了吗,然后c直接放到b后面就可以了,这样cpu在访问的时候,也只要访问三次(加起来就是10个字节了),但是这符合我上面说的那个规律(不能被4字节整除,哈哈哈,而且要注意的是,这里我是在32位环境下操作的,这个很关键,不同环境结果会不一样)。
下面我再举几个例子来演示(这里我就不仔细分析了,读者可以牛刀小试一下):
代码语言:javascript复制 #include <stdio.h>
struct mystruct1
{ // 1字节对齐 4字节对齐
int a; // 4 4
char b; // 1 2(1 1)
short c; // 2 2
};
struct mystruct21
{ // 1字节对齐 4字节对齐
char a; // 1 4(1 3)
int b; // 4 4
short c; // 2 4(2 2)
};
typedef struct myStruct5
{ // 1字节对齐 4字节对齐
int a; // 4 4
struct mystruct1 s1; // 7 8
double b; // 8 8
int c; // 4 4
}MyS5;
struct stu
{ // 1字节对齐 4字节对齐
char sex; // 1 4(1 3)
int length; // 4 4
char name[10]; // 10 12(10 2)
};
int main(void)
{
printf("the struct mystruct1 is %dn",sizeof( struct mystruct1 ));
printf("the struct mystruct21 is %dn",sizeof( struct mystruct21));
printf("the MyS5 is %dn",sizeof(struct myStruct5));
printf("struct stu is %dn",sizeof( struct stu));
return 0;
}
演示输出结果:
注意:
这里的第三个结构体输出结果怎么为32个字节了,注意我电脑是64位的。
4、gcc支持但不推荐的对齐指令:#pragma pack() #pragma pack(n) (n=1/2/4/8):
(1)#pragma是用来指挥编译器,或者说设置编译器的对齐方式的。编译器的默认对齐方式是4,但是有时候我不希望对齐方式是4,而希望是别的(譬如希望1字节对齐,也可能希望是8,甚至可能希望128字节对齐)。
(2)常用的设置编译器编译器对齐命令有2种:第一种是#pragma pack(),这种就是设置编译器1字节对齐(有些人喜欢讲:设置编译器不对齐访问,还有些讲:取消编译器对齐访问);第二种是#pragma pack(4),这个括号中的数字就表示我们希望多少字节对齐。
(3)我们需要#prgama pack(n)开头,以#pragma pack()结尾,定义一个区间,这个区间内的对齐参数就是n。
(4)#prgma pack的方式在很多C环境下都是支持的,但是gcc虽然也可以,不过不建议使用。
代码语言:javascript复制 #include <stdio.h>
#pragma pack(1)
struct mystruct1
{ // 1字节对齐 4字节对齐
int a; // 4 4
char b; // 1 2(1 1)
short c; // 2 2
};
struct mystruct21
{ // 1字节对齐 4字节对齐
char a; // 1 4(1 3)
int b; // 4 4
short c; // 2 4(2 2)
};
struct myStruct5
{ // 1字节对齐 4字节对齐
int a; // 4 4
struct mystruct1 s1; // 7 8
double b; // 8 8
int c; // 4 4
}MyS5;
struct stu
{ // 1字节对齐 4字节对齐
char sex; // 1 4(1 3)
int length; // 4 4
char name[8]; // 8 12(10 2)
};
#pragma pack()
int main(void)
{
printf("the struct mystruct1 is %dn",sizeof( struct mystruct1 ));
printf("the struct mystruct21 is %dn",sizeof( struct mystruct21));
printf("the MyS5 is %dn",sizeof(struct myStruct5));
printf("struct stu is %dn",sizeof( struct stu));
printf("%d",sizeof(struct mystruct1));
return 0;
}
输出演示结果:
说明:
通过实验现象,可以看到我设置了1字节(读者也可以尝试设置一下其他字节对齐,看看结果如何)。
5、gcc推荐的对齐指令__attribute__((packed)) __attribute__((aligned(n))):
(1)__attribute__((packed))使用时直接放在要进行内存对齐的类型定义的后面,然后它起作用的范围只有加了这个东西的这一个类型。packed的作用就是取消对齐访问。
代码语言:javascript复制 #include <stdio.h>
struct mystruct1
{ // 1字节对齐 4字节对齐
int a; // 4 4
char b; // 1 2(1 1)
short c; // 2 2
}__attribute__((packed));
int main(void)
{
struct mystruct1 s2;
printf("s2 is %dn",sizeof( s2));
return 0;
}
演示结果:
(2)__attribute__((aligned(n)))使用时直接放在要进行内存对齐的类型定义的后面,然后它起作用的范围只有加了这个东西的这一个类型。它的作用是让整个结构体变量整体进行n字节对齐(注意是结构体变量整体n字节对齐,而不是结构体内各元素也要n字节对齐)。
代码语言:javascript复制 #include <stdio.h>
typedef struct mystruct111
{ // 1字节对齐 4字节对齐 2字节对齐
int a; // 4 4 4
char b; // 1 2(1 1) 2
short c; // 2 2 2
short d; // 2 4(2 2) 2
}__attribute__((aligned(32))) My111;
int main(void)
{
printf("My111 is %dn",sizeof(My111 ));
return 0;
}
演示输出结果:
二、.offsetof宏与container_of宏:
1、通过结构体整体变量来访问其中各个元素,本质上是通过指针方式来访问的,形式上是通过.的方式来访问的(这时候其实是编译器帮我们自动计算了偏移量)。
代码语言:javascript复制 #include <stdio.h>
struct mystruct
{
char a;
int b;
short c;
};
int main(void)
{
struct mystruct s1;
s1.b = 12;
int *p = (int *)((char *)&s1 4);
printf("*p = %d.n", *p);
printf("整个结构体变量的首地址:%p.n", &s1);
printf("s1.b的首地址:%p.n", &(s1.b));
printf("偏移量是:%d.n", (char *)&(s1.b) - (char *)&s1);
return 0;
}
演示结果:
2、offsetof宏:
(1)offsetof宏的作用是:用宏来计算结构体中某个元素和结构体首地址的偏移量(其实质是通过编译器来帮我们计算)。
(2)offsetof宏的原理:我们虚拟一个type类型结构体变量,然后用type.member的方式来访问那个member元素,继而得到member相对于整个变量首地址的偏移量。
代码语言:javascript复制#include <stdio.h>
struct mystruct
{
char a;
int b;
short c;
};
// TYPE是结构体类型,MEMBER是结构体中一个元素的元素名
// 这个宏返回的是member元素相对于整个结构体变量的首地址的偏移量,类型是int
#define offsetof(TYPE, MEMBER) ((int) &((TYPE *)0)->MEMBER)
int main(void)
{
struct mystruct s1;
s1.b = 12;
int *p = (int *)((char *)&s1 4);
printf("*p = %d.n", *p);
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;
}
演示结果:
说明:
(TYPE *)0 这是一个强制类型转换,把0地址强制类型转换成一个指针, 这个指针指向一个TYPE类型的结构体变量。(实际上这个结构体变量可能不存在,但是只要我们不去解引用这个指针就不会出错)。((TYPE *)0)->MEMBER (TYPE *)0是一个TYPE类型结构体变量的指针,通过指针来访问这个结构体变量的member元素,&((TYPE *)0)->MEMBER 等效于&(((TYPE *)0)->MEMBER),意义就是得到member元素的地址。但是因为整个结构体变量的首地址是0,所以就可以计算出它的偏移量来。
3、container_of宏:
(1)作用:知道一个结构体中某个元素的指针,反推这个结构体变量的指针。有了container_of宏,我们可以从一个元素的指针得到整个结构体变量的指针,继而得到结构体中其他元素的指针。
(2)typeof关键字的作用是:typepef(a)时由变量a得到a的类型,typeof就是由变量名得到变量数据类型的。
(3)这个宏的工作原理:先用typeof得到member元素的类型定义成一个指针,然后用这个指针减去该元素相对于整个结构体变量的偏移量(偏移量用offsetof宏得到的),减去之后得到的就是整个结构体变量的首地址了,再把这个地址强制类型转换为type *即可。
代码语言:javascript复制 #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); // p就是指向结构体中某个member的指针
printf("s1的指针等于:%p.n", &s1);
// 问题是要通过p来计算得到s1的指针
pS = container_of(p, struct mystruct, c);
printf("pS等于:%p.n", pS);
return 0;
}
演示结果:
说明:
其中代码难以理解的地方就是它灵活地运用了0地址(这个零地址可以看c专题之指针---野指针和空指针解析,还有const的位置运用,可以看超实用的const用法)。如果觉得&( (struct mystruct *)0 )->member这样的代码不好理解,那么我们可以假设在0地址分配了一个结构体变量struct mystruct a,然后定义结构体指针变量p并指向a(struct mystruct*p = &s1),如此我们就可以通过&p->c获得成员地址的地址。由于a的首地址为0x0,所以成员c的地址就如上图所以。
三、总结 :
上面的那个空结构体的问题,在实际中作用不大,了解即可!今天的分享就到这里了,晚安!(有关柔性数组的问题暂时先不讲),这里也可以看这边博客关于结构体对齐讲的比较细:https://www.cnblogs.com/dolphin0520/archive/2011/09/17/2179466.html。