结构是一些值的某些集合,这些值称为成员变量。结构的每个成员可以是不同类型的变量。
这是建立在我们已经掌握结构体的基本使用之上,并且深入探究的一个问题:计算结构体的大小。即:结构体内存对齐(常考)
知识的运用往往是建立在联系的基础之上的,那么,我们就从以下四个练习题开始入手:
//练习一
struct S1
{
char c1;
int i;
char c2;
};
int main()
{
struct S1 s;
printf("%d\n", sizeof(s));
}
在我们开始入手这个之前,我们知道,char,int 分别是一个字节和四个字节,那么,这个结构体大小就是6个字节了吗?当然,在提出这个问题的时候就代表它一定是不对的,具体看一下运行结果:
既然答案不是6,而是12,那么12又是如何得来的呢? 通过上面的结构体,我们发现创建的顺序分别是c1->i->c2,那么内存的开辟也是按照这个顺序进行开辟的,char->int->char。在char已经开辟了一个字节之后,int如果接着下一个字节进行开辟,那么结果一定是6,故int一定不是接着char的下一个字节进行开辟的,通过反推我们发现:int在第五个字节开辟,即前四个字节中的第二三四个字节没有被使用,故我们知道了一个这样的规则:第一个成员变量在与结构体变量为0的地址处开辟,即char占用了0到1之间的字节。之后的成员变量要对齐到该成员变量占有字节大小的整数倍的位置上:
但是即便这样,仍然是9个字节,而不是12个字节,因此,还有一个这样的规则,结构体的大小为最大成员变量的整数倍。在这个结构体中,最大的成员类型为int,占四个字节,故在9个字节基础之上我们还应该加上3个字节,即该结构体占用了12个字节大小。
需要注意的是: 每一个成员的对齐数 = 编译器的默认对齐数与该成员对齐数的较小值,因此,在上述逻辑规则中,我们缺少了一部比较的步骤,int的对齐数需要与编译器默认的对齐数进行比较,选择小的那个,(以VS为例,VS中默认值为8),4<8,故此步骤对计算对齐数没有影响,但是仍然需要注意。
通过练习一的讲解,我相信大概都懂得怎么进行计算了,那么我们变换一下顺序:
//练习二
struct S2
{
char c1;
char c2;
int i;
};
int main()
{
struct S2 s;
printf("%d\n", sizeof(s));
}
第一个char无疑是在首字节上,第二个char大小为一个字节,1<8,故对齐数为1的倍数,所以接着第二个字节即可,第三个成员大小为int,占四个字节,4<8,故其对齐数应该为4的倍数,因此需要再跳过两个字节,在第五个字节开始开辟四个字节,故现在共占用了8个字节,8为最大对齐数4的整数倍,故此结构体的大小为8个字节。
那么我们改变一下类型继续练习:
//练习三
struct S3
{
double d;
char c;
int i;
};
int main()
{
struct S3 s;
printf("%d\n", sizeof(s));
}
从上到下依次计算,首先是double ,8<=8,从0开始占8个字节,ch然后是char,1<8,对齐数为1,故1的倍数即可,即接着第八个之后开辟一个字节,现在是9个字节,最后是int,4<8,对齐数为4,故我们需要在4的倍数开始创建Int,在9个字节的基础之上再跳过三个字节,然后开辟4个字节,9+3+4 =16个字节,全都开辟完成之后,我们知道结构体的大小必须是最大对齐数的倍数,16是8的倍数,故此结构体大小为16.
//练习四
struct S3
{
double d;
char c;
int i;
};
struct S4
{
char c1;
struct S3 s3;
double d;
};
int main()
{
struct S4 s;
printf("%d\n", sizeof(s));
}
通过练习三我们得知struct S3占16个字节,计算S4,从char 开始,1<8,取1在首位置,struct S3 为16 ,16>8,故取小的,对齐数为8,即跳过7个字节开辟,double 8<=8,对齐数为8,故此时对齐数为1+7(跳过字节数)+8+8 = 24,由于结构体大小为最大内部成员的对齐数的倍数,因此,为16的倍数,则为32。
上述提到VS默认对齐数为8,也就是说,不同的编译器的默认对齐数可能是不同的,因此我们引入pragma pack(),其能够将默认对齐数进行修改。
#pragma pack(4)
struct S
{
int i;//4 4 4 0~3
//4
double d;//8 4 4 4~11
};
#pragma pack()
int main()
{
struct S s;
printf("%d\n", sizeof(s));
}
括号内部为修改之后的默认对齐数,在结构体结尾处的pragma pack()的作用是截止修改,意思就是之后的结构体不会受到这里的影响,仍为8(VS). 那么修改之后计算得到的应该是4+8= 12,而double的对齐数仍为4,故最大对齐数为4,12为4的倍数,故结果仍为12。
以上的练习相信大家已经掌握这种问题的计算,那么如何验证我们计算的对齐数的方法是正确的呢?
通过offsetof调用我们能知道每一个内部成员对齐的位置,因此就能够验证出我们所计算的方法是否正确。
#define _CRT_SECURE_NO_WARNINGS 1
#include<stdio.h>
#include<stddef.h>
struct S1
{
char c1;//1
int i;//4
char c2;//1
};
struct S2
{
char c1;//1
char c2;//1
int i;//4
};
struct S3
{
double d;
char c;
int i;
};
struct S4
{
char c1;
struct S3 s3;
double d;
};
int main()
{
printf("%d\n", offsetof(struct S1, c1));
printf("%d\n", offsetof(struct S1, i));
printf("%d\n", offsetof(struct S1, c2));
printf("%d\n", offsetof(struct S2, c1));
printf("%d\n", offsetof(struct S2, c2));
printf("%d\n", offsetof(struct S2, i));
return 0;
}
由此证明,我们的计算方法是正确的。
如何计算:
为什么存在内存对齐:
总体来说:
结构体的内存对齐是拿空间换取时间的做法.
struct S
{
int data[1000];
int num;
};
void print1(struct S ss)
{
int i = 0;
for (i = 0; i < 3; i++)
{
printf("%d ", ss.data[i]);
}
printf("%d\n", ss.num);
}
void print2(const struct S* ps)
{
int i = 0;
for (i = 0; i < 3; i++)
{
printf("%d ", ps->data[i]);
}
printf("%d\n", ps->num);
}
int main()
{
struct S s = { {1,2,3}, 100 };
print1(s); //传值调用
print2(&s); //传址调用
return 0;
}
上面的print1和print2函数哪个好些?
原因:
结论:
struct A
{
//4byte-32bit
int _a : 2;
int _b : 5;
int _c : 10;
//15
//4byte-32bit
int _d : 30;
};
//
//47 bit
//6byte - 48bit
//8byte - 64bit
//
int main()
{
printf("%d\n", sizeof(struct A));
return 0;
}
以上述代码为例,首先我们通过上面的学习认识到,结构体的大小是4*4=16的可能性最先被排除掉,由于引进后面的数字,使其变成相应比特位的大小,如果只是单纯的比特位相加,总和47个比特位,看成48个比特位,就是6个字节大小,但是,这么单纯的相加,也是不对的。计算方法如下: *
//一个例子
struct S
{
char a : 3;
char b : 4;
char c : 5;
char d : 4;
};
int main()
{
struct S s = { 0 };
printf("%d\n", sizeof(struct S));
s.a = 10;
s.b = 12;
s.c = 3;
s.d = 4;
return 0;
}
//空间是如何开辟的呢?
当我们将其转化成二进制的时候,由于位段,需要舍弃其中的一部分比特位,当然,我们并不知道赋值放到内存中是从左到右还是从右到左,故我们假设是从右到左,二进制变成16进制,数字变成:62 03 04 。如上图,那么开始调试转成内存:(在VS编译器)
>故,我们的假设是正确的。
enum Day//星期
{
Mon,//0
Tues,//1
Wed,//2
Thur,//3
Fri,//4
Sat,//5
Sun//6
};
enum Day//星期
{
//枚举常量
Mon=1,
Tues,
Wed,
Thur,
Fri,
Sat,
Sun
};
为什么使用枚举?
枚举的优点:
联合也是一种特殊的自定义类型。 这种类型定义的变量也包含一系列的成员,特征是这些成员共用同一块空间(所以联合也叫共用体)。 比如:
//联合类型的声明
union Un
{
char c;
int i;
};
//联合变量的定义
union Un un;
//计算连个变量的大小
printf("%d\n", sizeof(un));//4
联合的成员是共用同一块内存空间的,这样一个联合变量的大小,至少是最大成员的大小(因为联合至少有能力保存最大的那个成员)。
union Un
{
int i;
char c;
};
union Un un;
// 下面输出的结果是一样的吗?
printf("%d\n", &(un.i));
printf("%d\n", &(un.c));
//下面输出的结果是什么?
un.i = 0x11223344;
un.c = 0x55;
printf("%x\n", un.i);
由于联合体的特性,共用一块空间,这就导致他们的首地址是相同的,当我们赋值时,也会由于覆盖的原因,后面赋值在公共的空间会将前面赋值的空间所覆盖,从而导致数的变化。
我们看到,u.c在u.a的后面赋值,则其会将对应公共的部分进行修改,刀子u.a变成了0x11223300,u.c = 0x00000000;当我们调换一下前后的位置时,发现:
u.a的公共部分将u.c的部分进行了修改,由此可见,联合体的公共部分是可以由后者改变前者的。
比如:
union Un1
{
char c[5];
int i;
};
union Un2
{
short c[7];
int i;
};
//下面输出的结果是什么?
printf("%d\n", sizeof(union Un1));
printf("%d\n", sizeof(union Un2));
首先,对于数组的处理,不能将他看成是void [n]进行计算,对于Un1来讲,char c[5]应该看成char c1,c2,c3,c4,c5;因此占用5个字节,而int占用四个字节。
现在是五个字节,要变成最大类型的整数倍,因此sizeof(union Un1)的大小为8,;同理,Un2的大小为16.
通过以上的对自定义类型的详解,可以让我们根据实际情况和具体的需求来节省空间和时间上的消耗,从而获得最大的效益。好了,本篇文章的分享到此结束了,码字不易,你们的支持将是我坚持的不竭动力。