目录
结构体声明:
结构体内存存储相关介绍:
结构体的初始化与使用:
结构体的初始化:
结构体的使用:
结构体对齐:
结构体对齐原则解释:
结构体对齐存在的原因:
#pragma pack( ):
offsetof讲解:
位段:
什么是位段?
位段的注意事项:
位段跨平台问题:
位段的实际运用:
枚举:
枚举的优点:
联合(共用体)
联合体判断大小端存储:
结构是一些值的集合,这些值成为成员变量。结构的每个成员可以是不同类型的变量。
结构体声明:
假设要用结构体描述一个人
struct Peo //Peo为结构体名字,是由自己来取的
{char name[20]; //名字char tele[12]; //电话号码char sex[5]; //性别int high; //身高
};
如名字、电话号码等就称之为结构的成员变量。并且结构的成员可以是标量、数组、指针,甚至是其他结构体。
当我们需要创建四个人,p1、p2、p3和p4,那我们应该如何创建呢?
struct Peo
{char name[20]; char tele[12];char sex[5]; int high;
}p1,p2;int main()
{……;struct Peo p3,p4;return 0;
}
上述代码中跟在结构类型创建后的2个变量,是作为全局变量而存在;在mian( )函数中创建的则是作为局部临时变量而存在。
结构体内存存储相关介绍:
结构类型创建后并不会占用内存空间,它只是作为一种类型而存在;而像上述代码中的p1、p2等变量会在内存中开辟一个空间进行存放,是作为一种变量存在
类比记忆:
结构类型是房屋建造的图纸,而结构体变量是实实在在存在的房子
结构体的初始化与使用:
笔者在此先创建了以下两个结构类型,第二个结构类型存在了结构体嵌套的现象
struct Peo
{char name[20]; char tele[12];char sex[5]; int high;
}struct St
(struct Peo p;int num;float f;
};
结构体的初始化:
int main()
{……;struct Peo p1 = {"zhangsan", "15596668862", "男", 181};struct St p2 = { {"lisi", "15596668888", "女", 168},100,3.14f};return 0;
}
结构体的使用:
如果需要使用上述代码中 zhangsan 这个名字,则需要 p1.name;如果需要使用 lisi这个名字,则需要 p2.p.name 。类似于数组下标,就是对结构体元素的指向。
而对于结构体的使用,共有两种方式来表示,其一为上述的 结构体变量.成员变量、结构体指针->成员变量。
函数传参时,参数是需要压栈的
如果传参一个结构体对象的时候,结构体过大,参数压栈的系统开销比较大,所以会导致性能的下降;同时,因为传参以后,函数需要创建形式参数,所以在传参以后,如果结构体过大,实参字节大小过大,形参内存占用也同样会比较多,这也就致使了内存空间的浪费。
而传参时如果传的是结构体指针,它是指向一个结构体整体的指针,只有4或8字节(不同环境下编译运行的区别),相较于结构体变量,大大节省了空间。
综上所述,结构体传参的时候,要传结构体的地址。
而我们在声明结构体的时候,可以不完全的声明。
比如:
struct
{int a;char b;float c;
}x;struct
{int a;char b;float c;
}a[20],*p;
上面的两个结构在声明的时候省略掉了结构体标签,而对于这种匿名结构体类型,会有以下两种问题:
1.结构体类型声明完以后,结构体变量只能在一处创建,即在结构体类型声明后创建
2.对于 p = &x ,编译器会把上面的两个声明当成完全不同的两个类型,所以是非法的
结构体对齐:
而在结构体的使用中,我们需要掌握结构体的对齐规则:
1.第一个成员在与结构体变量偏移量为0的地址处
2.其他成员变量要对齐到某个数字(对齐数)的整数倍的地址处
对齐数 = 编译器默认的一个对齐数与该成员大小的较小值
- 例如Visual studio中默认的值为8
3.结构体总大小为最大对齐数(每个成员变量都有一个对齐数)的整数倍
4.如果嵌套了结构体的情况,嵌套的结构体对齐到自己的最大对齐数的整数倍处
5.结构体的整体大小就是所有最大对齐数(含嵌套结构体的对齐数)的整数倍
6.数组可以看成某些个单个变量的集合,拆分开来,不难发现数组的对齐数就是它的类型对齐数
7.假设偏移量为0的地址处为0x0012ff40,那么偏移量为1的地址处为0x0012ff41
结构体对齐原则解释:
struct S2
{char c1;int i;char c2;
}s2;
//于vscode2022中进行的
如上述代码,c1作为字符型变量,占用1字节,vscode的默认对齐数为8,1字节小于8字节,因此对齐数为1;同理 i 应该对齐数为4,c2对齐数为1
注:蓝色地址存放的是c2,绿色地址存放的是i
在结构体存放时,除第一个成员变量以外,其余的成员变量要对齐到自身对齐数的整数倍处;例如,i的对齐数为4,那么就需要存放在4、8、12、4n(n >= 4, n Z) 位置处,两变量之间余留的直接舍去
而首成员变量存放的偏移为0的位置处可以看成是任何数的0倍。
同时,该结构体的最大对齐数为4,即i的对齐数,所以偏移量8可以作为结构体的整体大小,那么该结构大小为9字节
struct S3
{char c1;struct S2 s2 //其中的最大对齐数为4char c2;
};
对于上述代码,笔者在刚刚已经介绍过嵌套了结构体的情况,嵌套的结构体对齐到自己的最大对齐数的整数倍处;因此就是从偏移量为4位置处开始存放,存放到偏移量12处,再存放一个c2,整体大小为4的倍数,结果为17字节
结构体对齐存在的原因:
1.平台原因(移植原因):
不是所有的硬件平台都能访问任意地址上的任意数据的;某些硬件平台只能在某些地址处取某些特定类型的数据,否则抛出硬件异常。
2.性能原因:
数据结构(尤其是栈)应该尽可能地在自然边界上对齐。
原因在于,为了访问未对齐的内存,处理器需要作两次内存访问;而对齐的内存访问仅需要一次访问。
总而言之,
结构体的内存对齐就是拿空间来换取时间的做法
因此我们在设计结构体的时候,如果要既做到对齐,又要节省空间,就应该
让占用空间小的成员尽量集中在一起
#pragma pack( ):
作用是调整默认对齐数,例如#pragama pack(1)就是在说默认对齐数为1
offsetof讲解:
offsetof是用来看某结构体成员变量的存放偏移量的
例:
还是以上述代码为例,offsetof(struct s2, c2)结果为8,因其偏移量为8;offsetof(struct s2, i)结果为4,因其偏移量为4
位段:
什么是位段?
上述代码当中,整型变量应该为4字节,即32比特;而_a: 2就是将a这个变量变成2比特
有时候,我们不需要某个变量占用很大空间(例如某个变量只会是0或1),但是直接开辟必定会占用32比特,这种情况下我们就需要使用位段
而上述代码中是以整型大小为标准来开辟空间的 因此先开辟了32比特,然后存放了 2+5+10=17 以后,再存放30超过了32比特;因此又开辟了32比特的内存空间,存放30比特的数据
在上述代码中,先是开辟了8比特,存放7比特内容;然后又开辟了8比特,存放5比特内容;最后开辟了8比特,存放4比特内容;因此总共开辟了3字节
而对究竟体现出来是几,编译器是根据16进制来判断的,并且以2进制存入数据,每个数据占多少比特已经规定好了,计算时将四个比特位划分成一份来计算;如上图,编译器是从左往右来计算,从右往左来存放;先是a为10,二进制为1010,有3个比特位来存放,因此存入内存以后为010;b为12,存入1100,剩余1比特舍弃;c为3,存入011,剩下空间的直接舍弃;d为4,与上同理
计算时,以四个比特位为一份,先是第一个字节,结果是 2^2 + 2^1 = 6,2^1 = 2;以此类推,结果为62 03 04
位段的注意事项:
1.位段的成员可以是int、unsigned int、signed int 或者 char(属于整型家族)类型
2.位段上的空间是按照需要以4个字节(int)或1个字节(char)的方式来开辟的
3.在使用位段时,最好只创建一种变量的位段
4.位段所改变的大小必须 ≤ 其类型本身,例如整型数据最多可以设置其为32比特
5.位段涉及很多不确定因素,位段是不垮平台的,注意需要移植的程序应该避免使用位段
位段跨平台问题:
1.int位段被当成有符号数还是无符号数是不确定的
2.位段中的最大位数目不确定(整型数据在16位机器中最大为16比特;而在32位机器中为32比特)
3.位段中的成员在内存中从左往右分配还是从右往左分配尚未定义
4.当一个结构包含两个位段,第二个位段成员较大,无法容纳于第一个位段剩余的位时,是舍弃剩余的还是继续利用,这是不确定的
位段的实际运用:
例如网络传输,一个数据从我这传到别人电脑里是需要通过复杂的网络传输的,如下图所示
在这一过程中,所需要的比特大小各不相同;因此需要位段来控制大小,加快传输速度
枚举:
形如上述代码即为枚举,笔者在此枚举了星期,分别是星期一至星期日
enum Day 作为自定义类型存在,因此可以看成一种数据类型,创建变量时也是
数据类型 变量名称 = 某成员变量
如上代码,周一到周三打印出来分别为 0、1、2;这表明了枚举中的成员是从0开始往后延续;若我们想让星期一打印出来是1,我们可以进行如下操作
这时,打印结果如下
枚举的优点:
1.增加代码的可读性和可维护性
2.和#define定义的标识符比较,枚举有类型检查,更加严谨
3.防止命名污染(封装)
4.便于调试
5.使用方便,一次可以定义多个变量
#define M 100
int i = M
如上代码,机器在编译时会变成100,M消失了;然而通过枚举的方法,定义的成员变量本身不会消失
联合(共用体):
联合是一种特殊的自定义类型
这种类型定义的变量也包含一系列的成员,而这些成员共用内存中的同一块空间(因此也叫共用体)
如上述代码,如果我们要打印两个变量的地址以及整个联合体的大小
最后的结果为
原因解释:
在上述代码中,先是存放了整型变量a,共4字节;然后存放了字符型变量c,1字节,和已经开辟好的4字节共用同一个空间;因此上述代码中a、c的地址为同一个,同时联合体大小为4
注:联合的成员是共用同一块空间的,这样一个联合变量的大小,至少是最大成员的大小(因为联合至少得有能力储存最大的那个成员)
联合体判断大小端存储:
在讲解大小端以前,我们先要了解好大小端的概念
假设存在一个整型变量a,且 a = 1,那么他存放在内存空间中即为0x 00 00 00 01
如果是
01 00 00 00 //由低位到高位
即为小端
如果是
00 00 00 01 //由低位到高位
即为大端
可以通过上述代码来判断计算机存储是大端还是小端
check_sys( )函数共有以下两种实现方式:
1.强制转换方法
int a = 1;
return *(char*)&a; //先将a的地址取出,即01 00 00 00 或者 00 00 00 01
//然后判断最开始的两位,所以应该是1字节的元素,即char型,因此
//将a变量强制转换成char*,现在是 01 或 00,那么返回该地址数据
//结果为 0 或 1
2.联合体方法
union Un
{
char c; //先是 01 00 00 00,然后取出 01
int i;
}u;
u.i = 1;
return u.c;
}
注:联合体也需要满足结构体对齐原则
例如
union Un
{char arr[6];int i;
};
在开辟了6字节空间以后,需要进行对齐,因此总共开辟了9字节