在结构体章节,我们掌握了结构体的基本使用,但是现在我要你去计算一个结构体的大小,你会怎么做呢?
c1
、c2
、i
三个成员变量,那此时分别去计算它们两个结构体的大小, 最后的结果会是多少呢?会是一样的吗struct S1 {
char c1;
int i;
char c2;
};
struct S2 {
char c1;
char c2;
int i;
};
int main(void)
{
printf("%d\n", sizeof(struct S1));
printf("%d\n", sizeof(struct S2));
return 0;
}
c1
的类型为【char】,是1个字节;i
的类型是【int】,是4个字节c2
的类型为【char】,是1个字节;1 + 4 + 1 = 6B
,可事实呢,原不止这些。。。offsetof
,它可以用来计算结构体成员相对于起始位置的偏移量printf("%d\n", offsetof(struct S1, c1));
printf("%d\n", offsetof(struct S1, i));
printf("%d\n", offsetof(struct S1, c2));
为什么会出现上面这样的现象呢?对于结构体内存对齐的规则是怎样,让我们继续看下去
知晓了上面这些规则后,我们再来回顾一下上面这个结构体的大小该如何计算
ss
,它的起始地址就从0开始,所以根据第一条规则,第一个成员变量在与结构体变量偏移量为0的地址处,而且它的类型还是char
,所以只占1个内存单元i
,其为整型所以在内存中就需要存储4个字节的大小,此时便要拿其和VS下默认对齐数8去进行比较,取较小的值4i
是从4的位置开始放的,中间空出来的位置就不会再放置其他成员变量了,那么这个3个空间也就浪费了c2
,char类型的变量为1个字节,和8比较取小就是1,那就要将其放到1整数倍的地址处,那其实任何空间都是可以的,直接放到这个【8】的位置就行看完了,这个结构体后,还记得结构体S2吗,我再来讲一道,当然你也可以试着自己写写画画看
c1
放在这个与结构体变量偏移量为0的地址处,而且它的类型还是char
,所以只占1个内存单元char
所占的字节为1B,与8去进行比较一下就可以知道1来得小,那我们直接放在偏移处为1的地方就可以了,此时在内存中也只占了2个字节通过上面两道例题的讲解,相信你对如何去计算结构体大小一定有了一个自己的认识,接下去就让我们趁热打铁来做两道题目再练一练,看看自己是否真的掌握了
你可以先试着自己做一做,然后和我对一下是否正确
struct S3
{
double d;
char c;
int i;
};
printf("%d\n", sizeof(struct S3));
【分析】:
double
类型的数据在内存中占8个字节,所以一直占用偏移处为7的地方char
,所以在内存中占用1个字节,那直接放在偏移量为8的地址处即可接下去再来做一道练习,涉及结构体嵌套的问题,对应的需要使用到规则4,忘记了可以翻上去看看
struct S3
{
double d;
char c;
int i;
};
struct S4
{
char c1;
struct S3 s3; //成员变量为另一个结构体
double d;
};
因为本题的结构体比较大,所以就标出4的整数倍所在的地址
char
,所以只占一个字节的空间double
类型的数据在内存中占8个字节,所以一直到31的地址处运行结果如下:
经过了两道例题和两道练习题的训练,相信你对如何计算结构体的大小一定是心中有数了,但在阅读的过程中你是否有疑惑为什么会存在这个【结构体内存对齐】呢?有什么实际意义吗?
c
和i
,然后要在内存中存储它们,我分为了两种,一个是【无内存对齐】,呈现的是紧密存放;一个是【内存对齐】,需要考虑到最大对齐数c
,但是若要全部读取完i
,就还需要再读取一次,那访问到所有的成员变量就需要两次;c
和i
互不干扰,此时再看到成员变量i,从它的初始地址处开始读取,一次读4个字节,那么读1次就刚刚好可以读完这个变量了,而不是像上面那样还需要再读一次总体来说:
结构体的内存对齐是拿空间来换取时间的做法
了解了为什么会存在内存对齐之后,我们再回到一开始的这两个结构体,你是否有想过为什么两个结构体的成员变量都一模一样但是大小却是一个【12】,一个【8】呢?
struct S1 {
char c1;
int i;
char c2;
};
struct S2 {
char c1;
char c2;
int i;
};
之前我们见过了 #pragma 这个预处理指令
#pragma comment
,用来链接函数的静态库。这里我们再次使用,可以改变我们的默认对齐数
#pragma pack(1)
就可以设置默认对齐数为1,#pragma pack()
就可以取消设置的默认对齐数,还原为默认。到它为止的默认对齐数还是被修改后的对齐数#pragma pack(1)//设置默认对齐数为1
struct S1
{
char c1;
int i;
char c2;
};
#pragma pack()//取消设置的默认对齐数,还原为默认
int main()
{
//输出的结果是什么?
printf("%d\n", sizeof(struct S1));
return 0;
}
可以通过【offsetof】再来验证一下
在上面的每一个结构体计算后,我都使用到了
offsetof
这个宏,和我画出来的内存分布图完全就是一致的,那它的原理到底是怎样的呢?马上来探究一下
曾经有一年的百度笔试题就考到了有关offsetof
的实现原理
【原题】:写一个宏,计算结构体中某变量相对于首地址的偏移,并给出说明
那要如何去实现呢?如果对宏不是很了解的读者可以看看详解程序环境和预处理
我们通过上面的结构体S1进行讲解。列出3个成员变量放置的初始地址,其实【offsetof】计算的也就是每个变量在内存中的起始地址相较于首地址偏移了多少,那将它们进行一个相减就可以得出0
、4
、8
这三个结果
c1
这块地址设置为0,那么
&c1 - 0
&i - 0
&c2 - 0
知道了上面这些我们就可以使用【宏】来实现每个成员变量偏移量的计算了
#define OFFSETOF(m_type, m_name) (int)&(((m_type *)0)->m_name)
m_type
是结构体变量;m_name
是结构体成员
#define OFFSETOF(m_type, m_name) (m_type *)0
printf("%d\n", OFFSETOF(struct S1, c1));
m_name
#define OFFSETOF(m_type, m_name) ((m_type *)0)->m_name
#define OFFSETOF(m_type, m_name) &(((m_type *)0)->m_name)
#define OFFSETOF(m_type, m_name) (int)&(((m_type *)0)->m_name)
下面是流程图:
结构体怎么对齐? 为什么要进行内存对齐?
如何让结构体按照指定的对齐参数进行对齐?能否按照3、4、5即任意字节对齐?
#pragma pack(3)
便可以将默认对齐数修改为3,其他的也是同理,因为结构体默认对齐数发生了变化,此时就会导致结构体大小发生变化最后来总结一下本文所学习的内容
offsetof()
,但是不清楚原理是什么这不,百度笔试题就考到了,于是我们就去自己通过一个宏实现了一下这个偏移量的求解,虽然过程很复杂,但是在我一步步的细讲下,相信聪明的你一定有所理解在理解了结构体内存对齐的各方面之后,面对两道面试题也是毫不畏惧可以发现仅仅是非常小的一个知识点,我却讲解了近万字,因为这是校招的笔试面试中C语言这一块的热门考点,如果有投递相关岗位的同学一定要搞清楚每一步