一、结构体
在C语言中有int,char,float等等类型,可以用来形容某些数据,但是有些数据仅靠一种类型无法描述出来,比如说一个人,我们不仅要描述他的名字,还要描述他的身高、体重、性别等特征。此时我们我发一种C语言自带的内置类型定义一个人。此时我们就需要学会使用结构体。
1、什么是结构体
结构体是一些值的集合,这些值被称为结构体的成员变量,这些成员变量类型可以各不相同。可以将结构体理解为我们自己定义的一种类型,和int,char这些类型类似,但是结构体可以用来描述更加抽象的数据。
2、结构体的声明
struct是结构体关键字,在声明结构体是是不可缺少的。tag指的是结构体标签,也就是结构体的名字。member-list是标准的变量定义,在这里指的就是成员变量。variable-list是结构体变量。
如果我们要用结构体描述一个学生。
3、特殊的结构体声明
上面这个结构体在声明时舍弃了结构体标签,此时我们想要使用这个结构体类型,就只能在声明时定义,如上,我们在声明结构体时,定义了结构体变量
People1、People2。
4、结构体的定义与初始化
结构体变量的定义无非就是两种方式:
结构体变量的初始化:
5、结构体内存对齐
接下来讨论结构体的大小,看下面一题,求打印出的结果。
S1的第一个成员变量是int型,占4个字节,第二个和第三个成员变量char型,各占1个字节,加起来一共是6个字节。S2的成员变量只是顺序不同,应该也是6个字节。打印结果应该是6和6。
答案如下:
答案并不是我们想象的那样。
而且S1和S2的大小是不一样的,这就意味着结构体成员变量的顺序会影响结构体的大小。
先要求结构体的大小,就必须先了解结构体对齐。
结构体对齐规则如下:
①第一个成员在与结构体变量偏移量为0的地址处。
②其他成员变量要对齐某个数字(对齐数)的整数倍的地址处。
对齐数=编译器默认的一个对齐数与该成员变量大小的较小值。
VS编译器中默认的对齐数是8.
③结构体总大小为最大对齐数(每个成员变量对齐数的最大值)的整数倍。
④如果嵌套了结构体的情况下,嵌套的结构体对齐到自己的最大对齐数的整数倍处,结构体的整体大小就是所有最大对齐数(含嵌套结构体的对齐数)的整数倍。
多说无益,我们用,图来解释。
首先所谓的偏移量就是值离结构体地址起始处距离。
如图所示,1个格子代表1个字节。假设这个结构体的起始地址指向红色区域,那么这个红色区域的偏移量为0,黄色区域的偏移量为1,绿色区域的偏移量为2,以此类推。
我们先来看S1:
S1的第一个成员变量a是整形,根据规则①,应该从0偏移出开始,共占4个字节。用红色区域表示。
第二个成员变量b是字符型,根据规则②,b的大小是1个字节,VS默认对齐数是8,取较小值,所以b的默认对齐数是1。并且要存放在偏移量为1的倍数的地址处,4就是1倍数,我们绿色区域表示b。
第三个成员变量c与b一样,用黄色区域表示。
最后,根据规则3,三个成员变量中最大的对齐数是4,所以结构体的整体大小得是4的倍数,现在已经占了6个字节了,我们要浪费2个字节,使结构体的大小为8个字节。蓝色区域表示浪费掉的空间。
我们再来分析一下S2
S2的第一个成员变量b是字符型,根据规则①,应该从0偏移出开始,共占1个字节。用绿色区域表示。
第二个成员变量a是整型,根据规则②,a的大小是4个字节,VS默认对齐数是8,取较小值,所以b的默认对齐数是4。并且要存放在偏移量为4的倍数的地址处,4就是4倍数,我们跳过浪费掉偏移量为1、2、3的空间,我们红色区域表示a,蓝色区域表示浪费掉的空间
第三个成员变量c是字符型,根据规则②,c的大小是1个字节,VS默认对齐数是8,取较小值,所以c的默认对齐数是1。并且要存放在偏移量为1的倍数的地址处,8就是1倍数,这次就不用浪费空间。我们黄色区域表示c。
最后,根据规则3,三个成员变量中最大的对齐数是4,所以结构体的整体大小得是4的倍数,现在已经占了9个字节了,我们要浪费3个字节,使结构体的大小为12个字节。
6、内存对齐的意义
①平台原因(移植原因):
不是所有的硬件平台都能访问任意地址上的任意数据的;某些硬件平台只能在某些地址处取某些特定类型的数据,否则抛出硬件异常。
②性能原因:
数据结构(尤其是栈)应该尽可能地在自然边界上对齐。
原因在于,为了访问未对齐的内存,处理器需要作两次内存访问;而对齐的内存访问仅需要一次访问。
总体来说:
结构体的内存对齐是拿空间来换取时间的做法。
7、修改默认对齐数
可以用pragma这个预处理指令来修改默认对齐数
//修改对齐数
#pragma pack(1)
struct S1
{
char b;
int a;
char c;
};
int main()
{
printf("%d\n", sizeof(struct S1));
return 0;
}
我们把默认对齐数修改位1,根据我们上面所讲的知识,此时printf打印出来的结果应该是6。
S1的内存分布如下:
//修改对齐数
#pragma pack(1)
//重置默认对齐数
#pragma pack()
struct S1
{
char b;
int a;
char c;
};
int main()
{
printf("%d\n", sizeof(struct S1));
return 0;
}
将默认对齐数重置,此时默认对齐数为8,所以打印的结果应该是12
二、位段
1、什么是位段
位段的声明和结构体的声明类似,有两个地方不同。
①位段的成员必须是整形家族(如int、char等等)
②位段成员名后面有一个冒号和数字。
冒号后面的数字代表的是自己规定的这个数据所占的空间大小,单位是比特位。例如b这个成员变量只能占用2个比特位的空间。
2、位段的内存分配
①位段的成员可以是 int unsigned int signed int 或者是 char (属于整形家族)类型
②位段的空间上是按照需要以4个字节( int )或者1个字节( char )的方式来开辟的。
③位段涉及很多不确定因素,位段是不跨平台的,注重可移植的程序应该避免使用位段。
位段具有跨平台缺陷,在不同的平台上实现方式不同,我们现在就在VS2022的环境(小端)下探究一下位段的内存分配
这个位段里的成员是int型,系统会先开辟4个字节的空间,如下:
我们给成员a规定的大小是2个比特位,a的值为2,转换为二进制为10,放入第一个字节的空间,接下来我们要讨论一个问题,是要从左往右分配还是从右往左分配?也就是a应该放在哪?
下图红色的空间表示存放a的位置。
有两种可能,我们先假设是从右往左分配,最后验证是否正确。
我们给成员b规定的大小是30个比特位,此时内存的分配如下
我们给成员c规定的大小是3个比特位,系统第一次开辟的4个字节的空间已经用完了,此时系统会再次开辟4个字节的空间,存放c。
然后我们放入他们的值,a的值转化为二进制位是01(2位),b的值转化为二进制位是
00 0000 0000 0000 0000 0110 0001 1010(30位),c的值转化为二进制位是111(3位)。 注意,我们是在小端存储进行的,所以低位字节的数据要放在低地址处。
最后s的内存分配如下:
我们把每4个比特位的二进制位转化为16进制位
接下来我们通过调试来查看位段变量s内存中的储存情况,如果是
6A 18 00 00 00 07 00 00 00,那就代表我们的猜测是对的。
通过调试,我们证实了我们的猜想是正确的,在VS2022的环境下,空间是从右往左分配,并且我们也知道了,位段变量b所占的内存大小为8个字节。
我们再来看这题,当分配好成员a的空间时,成员变量a只占了3个比特位,还剩余了5个比特位的空间,那我们在给成员b分配空间时,我们需要7个比特位的空间,前面的5个比特位的空间显然不够,系统此时再给1个字节的空间,那么问题来了,我们是否要使用前面5个比特位的空间,还是说浪费5个比特位的空间,直接使用后面的空间。
图解如下:
和上一题一样,我们先假设,随后在通过调试来验证。
我们就先假设会浪费空间。
那么r的内存分布就应该如下:
我们在通过调试来查看位段变量r在内存中的储存情况:
分布情况和我们猜想的一样,这也就代表这,在VS2022的环境下,当第二个位段成员较大,无法容纳于第一个位段成员剩余的空间时,浪费掉剩余的空间。
并且我们得知了位段变量r所占空间大小为2个字节。
3、位段的跨平台问题
①int 位段被当成有符号数还是无符号数是不确定的。
②位段中最大位的数目不能确定。(16位机器最大16,32位机器最大32,写成27,在16位机 器会出问题。
③位段中的成员在内存中从左向右分配,还是从右向左分配标准尚未定义。
④当一个结构包含两个位段,第二个位段成员比较大,无法容纳于第一个位段剩余的位时,是舍弃剩余的位还是利用,这是不确定的。
4、位段的应用
位段相对于结构体最大的优点就是能够节省空间,例如,一个变量只需要存储0或或2或3。如果使用整形变量的话,就会多浪费掉30个比特位的空间。此时我们使用位段就能够很好地节省空间。
位段的缺点就是存在跨平台问题,所以我们在使用位段时一定要斟酌。
三、枚举
枚举顾名思义就是一一列举,例如一周中的7天从星期一到星期日可以一一列举。再比如人的性别可以列举出男或女。
1、枚举类型的声明
//枚举
//枚举关键字enum不能舍弃
enum Sex
{
male,
famale
};
//枚举也可以相结构体一样改名
typedef enum Country
{
China,
Americ,
Italy,
England,
Korea,
Japan
}Country;
以上定义的enum Sex、Country都是枚举类型。{}内的是枚举类型的所有可能取值,我们称之为枚举常量。每个枚举常量都有对应的值,默认从0开始,依次加1。例如,枚举常量China默认的值是0,America的默认值就是1,以此类推。
我们也可以在定义时就赋值。
enum Color
{
Red = 3,
Blue = 5,
purple,//值为前一个值+1,也就是6
Yellow = 11,
green,//值为前一个值+1,也就是12
};
2、枚举的优点
枚举和用#define定义常量所达成的效果十分类似,我们为什么要用枚举呢?
枚举的优点:
①增加代码的可读性和可维护性。
②和#define定义的标识符相比枚举有类型检查,更加严谨。
③防止了命名污染。
④便于调试。
⑤使用方便,一次可以定义多个常量。
关于第①点,举个例子
int main()
{
printf("1.add 2.sub\n");
printf("3.mul 4.div\n");
int option;
printf("请输入选项:");
scanf("%d", &option);
switch (option)
{
case 1:
printf("加法");
break;
case 2:
printf("减法");
break;
case 3:
printf("乘法");
break;
case 4:
printf("除法");
break;
defalut:
printf("错误");
break;
}
return 0;
}
这段代码没有任何问题,但如果但是当我们调试这段代码时忘记了1,2,3,4代表的是执行什么命令时,还得回去查看,在这段代码可能无法体现出这种问题,但如果是一段非常复杂的代码,这种问题就会非常的麻烦,此时我们就可以使用枚举。
enum Option
{
add=1,
sub=2,
mul=3,
div=4
};
int main()
{
printf("1.add 2.sub\n");
printf("3.mul 4.div\n");
enum Option option;
printf("请输入选项:");
scanf("%d", &option);
switch (option)
{
case add:
printf("加法");
break;
case sub:
printf("减法");
break;
case mul:
printf("乘法");
break;
case div:
printf("除法");
break;
defalut:
printf("错误");
break;
}
return 0;
}
此时代码就非常地直观了,增强了代码的可读性和可维护性。
关于第②点
#define只是简单地进行替换,并非定义一个类型,所以#define定义的标识符是没有类型可言的。但是枚举是有类型的,这也就意味着如果把非枚举类型的数据赋值给枚举变量时,是非法的。
关于第③点
当#define定义了一个标识符,那么其他地方就无法使用同名的标识符,这就被称为命名污染例如
编译器会报警告。
而使用枚举则不会有任何问题。
关于第④点
枚举类型的数据是可以在调试中观察的
能够更加方便我们进行调试。
四、联合体(共用体)
1、联合体的定义
联合体也是自定义类型的一种,联合体类型的变量也会由一个个成员构成,特点就是这些成员公用同一块内存。
//联合体
//联合类型的声明
union S1
{
char a;
int b;
};
int main()
{
//联合变量的定义
union S1 s;
//计算联合变量的大小
printf("%d", sizeof(s));
return 0;
}
2、联合体的特点
联合体成员是共用同一块内存空间,一个联合体变量的大小,至少得是最大成员的大小(至少得方的下最大的成员。)
我们用一段代码来验证。当我们分别求联合体变量s,以及s的两个成员变量的地址时,它们的地址都是一样的,这也就印证了联合体成员变量公用同一块内存空间。
3、联合体的内存分配
看下面这题
因为联合体的成员公用同一块内存空间,所以这个联合体只需要4个字节的空间就可以存放两个成员。
结果如下:
再看下面这一题
成员a是一个字符数组,共占6个字节,如果联合体变量s有6个字节的空间,那么放4个字节的成员b也不是问题,那么答案是6吗?
然而正确答案案是8。
为什么呢,因为联合体也是有对齐的,上面这题,成员a的对齐数是1,成员b的对齐数是4,那么这个联合体变量的大小就应该是成员中最大对齐数的整数倍,也就是8个字节。也就是说浪费了2个字节的空间。
下图,假设一个代表1个字节的空间
本文结束。