腾讯云网站建设视频教程,wordpress用户名或密码错误,黄山建设工程信息网站,济南网站建设排名一、结构体
前言#xff1a;有关结构体的声明、定义、初始化以及结构体的传参等结构体的基本使用在文章【逐步剖C】-第六章-结构体初阶中已进行了详细的介绍#xff0c;需要的朋友们可以看看。这里主要讲解的是有关结构体的内存问题。
1. 结构体的内存对齐
#xff08;1有关结构体的声明、定义、初始化以及结构体的传参等结构体的基本使用在文章【逐步剖C】-第六章-结构体初阶中已进行了详细的介绍需要的朋友们可以看看。这里主要讲解的是有关结构体的内存问题。
1. 结构体的内存对齐
1对齐规则与结构体大小的计算
我们知道一个结构体内部可能有多个不同类型的成员那么对于整个结构体而言它的大小怎么计算呢这就要涉及到结构体内存对齐这个重要知识点了 这里先放上结构体内存对齐的规则 1结构体的第一个成员对齐到与该结构体成员对比偏移量为0的地址处 2从第二个成员开始每个成员都要对齐到偏移量为其对应对齐数的整数倍的地址处 每个成员对应的对齐数 编译器默认的一个对齐数 与 该成员大小 的较小者。 VS中默认的值为8 Linux gcc中没有默认的对齐数对齐数就是结构体成员的自身大小 3结构体总大小为最大对齐数每个成员变量都有一个对齐数的整数倍。 4如果有嵌套了结构体的情况嵌套的结构体对齐到自己的最大对齐数的整数倍处结构体的整体大小就是所有最大对齐数含嵌套结构体的对齐数的整数倍。
下面将结合代码并配合示意图进行讲解。 代码1
struct S1
{char ch1;int i;char ch2;
};
printf(%d\n, sizeof(struct S1));运行结果 解释 首先结构的第一个成员为ch1其类型为char那么根据规则的第一条其将对齐到偏移量为0的地址处示意图如下 其次结构的第二个成员为i其类型为int根据规则的第二条其将对齐到偏移量为它对应的对齐数的整数倍的地址处又因为int类型的大小为4个字节编译器VS默认的对齐数为8取二者中的较小者故该成员i的对齐数为4故成员i将在成员ch1的基础上对齐到偏移量为4的整数倍的地址处示意图如下 接着结构体的第三个成员为ch2其类型为char同第二个成员i根据规则的第二条其将对齐到偏移量为它对应的对齐数的整数倍的地址处又因为char类型的大小为1个字节编译器VS默认的对齐数为8取二者中的较小者故该成员的对齐数为1故成员ch2将在前面成员的基础上对齐到偏移量为1的整数倍的地址处示意图如下 最后根据规则3在所有成员都对齐到对应地址处的基础上整个结构体的总大小为所有成员中最大对齐数的整数倍也就是4成员i的对齐数的整数倍故整个结构体的大小为12。
代码2:
struct S2
{char c1;char c2;int i;
};
printf(%d\n, sizeof(struct S2));运行结果:
解释 首先结构的第一个成员为c1其类型为char那么根据规则的第一条其将对齐到偏移量为0的地址处示意图如下 其次结构的第二个成员为c2其类型为char根据规则的第二条其将对齐到偏移量为它对应的对齐数的整数倍的地址处又因为char类型的大小为1个字节编译器VS默认的对齐数为8取二者中的较小者故该成员的对齐数为1故成员c2将在成员c1的基础上对齐到偏移量为1的整数倍的地址处示意图如下 接着结构体的第三个成员为i其类型为int同第二个成员c2根据规则的第二条其将对齐到偏移量为它对应的对齐数的整数倍的地址处又因为int类型的大小为4个字节编译器VS默认的对齐数为8取二者中的较小者故该成员的对齐数为4故成员i将在前面成员的基础上对齐到偏移量为4的整数倍的地址处示意图如下 最后根据规则3在所有成员都对齐到对应地址处的基础上整个结构体的总大小为所有成员中最大对齐数的整数倍也就是4成员i的对齐数的整数倍故整个结构体的大小为8。
代码3:
struct S3
{double d;char c;int i;
};
printf(%d\n, sizeof(struct S3));运行结果
解释 首先结构的第一个成员为d其类型为double那么根据规则的第一条其将对齐到偏移量为0的地址处又因为其本身大小为8个字节故其在内存中的示意图如下 其次结构的第二个成员为c其类型为char根据规则的第二条其将对齐到偏移量为它对应的对齐数的整数倍的地址处又因为char类型的大小为1个字节编译器VS默认的对齐数为8取二者中的较小者故该成员的对齐数为1故成员c将在成员d的基础上对齐到偏移量为1的整数倍的地址处示意图如下 接着结构体的第三个成员为i其类型为int同第二个成员c根据规则的第二条其将对齐到偏移量为它对应的对齐数的整数倍的地址处又因为int类型的大小为4个字节编译器VS默认的对齐数为8取二者中的较小者故该成员的对齐数为4故成员i将在前面成员的基础上对齐到偏移量为4的整数倍的地址处示意图如下 最后根据规则3在所有成员都对齐到对应地址处的基础上整个结构体的总大小为所有成员中最大对齐数的整数倍也就是8成员d的对齐数的整数倍故整个结构体的大小为16。
代码4
struct S4
{char c1;struct S3 s3;double d;
};
printf(%d\n, sizeof(struct S4));运行结果
解释 首先结构的第一个成员为c1其类型为char那么根据规则的第一条其将对齐到偏移量为0的地址处示意图如下 其次第二个成员s3为另一个结构体类型的变量该类型就是我们代码3中的结构体类型S3这里就涉及到结构体嵌套的情况那么结合规则4与对代码3的分析我们可得成员s3将对齐到偏移量为自己结构中最大对齐数也就是结构S3中成员d的对齐数为8的整数倍的地址处又因为成员本身的大小为16个字节故内存中的参考示意图如下 接着结构体的第三个成员为d其类型为double根据规则的第二条其将对齐到偏移量为它对应的对齐数的整数倍的地址处又因为double类型的大小为8个字节编译器VS默认的对齐数为8取二者中的较小者故该成员的对齐数为8故成员i将在前面成员的基础上对齐到偏移量为8的整数倍的地址处示意图如下 最后根据规则3在所有成员都对齐到对应地址处的基础上整个结构体的总大小为所有成员中最大对齐数的整数倍也就是16成员s3的对齐数的整数倍故整个结构体的大小为32。
2内存对齐的意义
通过如上讲解大家可能想问内存示意图中那些空出来的白色部分去哪了呢 答案是这部分的内存会为了完成结构体的对齐而浪费掉。 大家可能会追问到那么为了内存对齐而浪费这些内存空间真的有必要吗 那么接下来为大家介绍一下结构体内存对齐的意义
为什么会存在结构体对齐呢 通过大部分资料可总结出如下两个主要原因 平台原因(移植原因) 不是所有的硬件平台都能访问任意地址上的任意数据的某些硬件平台只能在某些地址处取某些特 定类型的数据否则抛出硬件异常。性能原因 数据结构(尤其是栈)应该尽可能地在自然边界上对齐。 原因在于为了访问未对齐的内存处理器需要作两次内存访问而对齐的内存访问仅需要一次访 问。 其中对与性能原因中的两次访问可以这么理解 如上面代码2中的这个结构体
struct S1
{char c1;int i;char c2;
};看下面这张内存示意图的对比 假设以4个字节一般是成员最大对齐数为单位进行内存访问那么对于内存对齐的情况由于中间的内存空间是不用的所以每次访问都能完整地读取到每个成员的内存信息
访问地址0 1 2 3得到成员ch1的信息访问地址4 5 6 7得到成员i的信息访问地址8 9 10 11得到成员ch2的信息
而对于内存内存不对齐的情况由于内存空间都是连在一起的所以在每次访问内存时都可能会出现“割裂” 访问的情况
访问地址0 1 2 3时既有成员ch1的信息又有成员i的信息故对于成员i而言需要两次访问才能完整地得到成员i内存中的信息。
如此看来采用内存对齐后更方便了对结构体成员内存的访问。 所以对于内存对齐总的来说就是牺牲一定的空间来换取时间上的效率。
那么我们在设计结构体的时候我们如何在内存对齐的情况下尽量地节省空间呢 答案是让占用空间小的成员尽量集中在一起。让上面的代码1与代码2结构体成员的类型相同但由于结构体成员在结构体中的位置不同导致最终结构体大小也不同。
3修改默认对齐数
上面提到不同编译器可能有不同的默认对齐数对这个默认对齐数其实我们可以通过一个预处理指令#pragma对其进行更改请看下面这段代码
#pragma pack(8)//设置默认对齐数为8
struct S1
{char c1;int i;char c2;
};
#pragma pack()//取消设置的默认对齐数还原为默认#pragma pack(1)//设置默认对齐数为1
struct S2
{char c1;int i;char c2;
};
#pragma pack()//取消设置的默认对齐数还原为默认int main()
{printf(%d\n, sizeof(struct S1));printf(%d\n, sizeof(struct S2));return 0;
}程序输出结果 这里的分析方法呢和1部分中的内容是相同的唯一的区别就在于编译器默认对齐数的改变导致了结构体成员最大对齐数的改变这里就不在赘述啦。 所以我们可以判断结构体对齐方式是否合适从而自己更改合适的默认对齐数。
2. 结构体实现位段
1位段的定义
什么是位段呢 位段C语言允许在一个结构体中以位为单位来指定其成员所占内存长度这种以位为单位的成员称为“位段”或称“位域”( bit field) 。利用位段能够用较少的位数存储数据。———百度百科 位段的声明方式与规则 位段的成员必须是 int、unsigned int 或signed char位段的成员名后边有一个冒号和一个数字 如下面这段代码
struct A
{int a:2;int b:5;int c:10;int d:30;
};结构体A就是一个位段类型那么位段A的总大小该如何计算呢请看下面一部分。
2位段的内存分配
这里先以上面的结构体A作为例子进行分析然后再介绍一些位段内存分配的细节问题 由位段的概念可知我们可以以位bit为单位来指定结构体成员所占的内存长度。 对于结构体A而言其占用内存的情况可以按以下过程进行理解 第一个成员int a:2;编译器先申请32个比特位然后给成员a分配2个比特位分配完成后剩余30个比特位第二个成员int b:5;编译器再从剩余的30个比特位中给成员b分配5个比特位分配完成后剩余25个比特位第三个成员int c:10;编译器再从剩余的25个比特位中给成员c分配10个比特位分配完成后剩余15个比特位第四个成员int d:30;编译器需要为其分配30个比特位但剩余的比特位不够分配的需求编译器会重新申请32个比特位并用这新申请的32个比特位来为其进行内存分配。 此时就会产生一个问题第一次申请中的剩余的15个比特位去哪了呢 可能舍弃了也可能保存起来了这里没有明确的标准规定所以是不确定的。回到过程中至此对每个成员的内存分配就完成了那么由如上过程我们得到为成员分配内存时共申请两次32个比特位总计也就是64个比特位8个字节。故结构体A最终的大小就为8个字节。我们也可通过打印进行验证
代码
int main()
{struct A{int a : 2;int b : 5;int c : 10;int d : 30;};printf(%d\n, sizeof(struct A));return 0;
}结果 注上面所谓内存分配的“过程”仅是一种理解方式实际中的内存空间是一次就开辟好了的这一点需要注意。
接下来说明一下位段在内存分配中的一点细节问题 请看下面这一段代码
struct S
{char a:3;char b:4;char c:5;char d:4;
};
struct S s {0};
s.a 10;
s.b 12;
s.c 3;
s.d 4;在1中我们介绍了一下内存分配的 “过程”接下来将通过编译器的调试带大家看看在对结构体成员进行赋值时内存中内容的实际变化PS接下来的内容需要用到机器大小端字节序存储的知识点如果有不太了解的朋友可以看看这篇文章【逐步剖C】-第七章-数据的存储。
执行完语句struct S s {0};的初始状态 解释可以看到整个结构体在内存中只占了3个字节的长度这里同样可以用1中的 “分配过程” 来理解编译器先申请8个比特位1个字节PS内存的开辟形式编译器会按需分配最后的小结中还会提到给成员a分配3个比特位后剩余5个比特位接着给成员b分配4个比特位后还剩1个比特位接着编译器新申请8个比特位分配给成员c后剩3个比特位由于剩余比特位又不够成员d的分配故最后编译器又再申请了8个字节分配给成员d。 综上编译器一共申请了3个字节的内存。故整个结构体在内存中的大小就为3个字节。
继续往下
执行完赋值语句s.a 10;后 解释 可以看到内存中第一个字节的内容被改为了02但按理来说成员a被赋值为了10第一个字节的内容应该是0a十六进制呀。这就是位段的效果了我们知道按8个比特位来看10的二进制序列为
0000 1010对于成员a而言其在内存中其实只占用了前3个比特位的内容即010理解起来就是 “截断往里存”故在内存中二进制序列的实际情况为
0000 0010这样换算成十六进制就是图中的02了。
执行完赋值语句s.b 12;后 解释 可以看到第一个字节的内容变为了62其中的原因和上面一样 首先12的二进制序列为
0000 1100由于成员b只占用4个比特位的内容所以截断为1100并在成员a的基础上往里存也就是内存中的二进制序列变为
0111 0010这样换算成十六进制就是图中的62了。
执行完赋值语句s.c 3;后 解释可以看到第二个字节中的内容变为了03由初始状态的解释可以知道在为成员c分配空间时新申请了一个字节成员c占用5个比特位的内存所以 “截断” 为00011往里存那么在前面的基础上我们从16个比特位来看此时内存中的二进制序列如下
0000 0011 0110 0010最后执行完赋值语句s.d 4;后 解释可以看到第三个字节的内容变为了04。同样由初始状态的解释可以知道在为成员d分配空间时又新申请了一个字节成员d占用4个比特位的内存所以 “截断” 为0100往里存那么在前面的基础上我们从24个比特位来看此时内存中的二进制序列如下
0000 0100 0000 0011 0110 0010这里大家可能会发现实际二进制序列的内容和编译器上显示出来的内容好像是反着的即
0000 0100 0000 0011 0110 0010
对应转为16进制应为
04 03 62
而编译器显示的内容为
62 03 04这是因为当前机器的存储形式为小端字节序存储低位的数据存储在低地址高位的数据存储在高地址对于这部分的详细介绍感兴趣的朋友们可以看看这部分开始时提到的那篇文章下面是示意图
小结 位段的成员可以是 int、unsigned int 、signed int 或者是 char 属于整形家族类型位段的空间上是按照需要以4个字节 int 32个比特位或者1个字节 char 8个比特位的方式来开辟的按需分配。 3位段的一些问题 int 位段被当成有符号数还是无符号数是不确定的。位段中最大位的数目不能确定。16位机器最大1632位机器最大32写成27在16位机 器会出问题。且一般来说位的指令不能超过自身类型的大小。位段中的成员在内存中从左向右分配还是从右向左分配标准尚未定义。当一个结构包含两个位段第二个位段成员比较大无法容纳于第一个位段剩余的位时是 舍弃剩余的位还是利用这是不确定的位段是不存在对齐的 解释一下其中的第三点 以2中的例子为例在为结构体成员a和b分配空间时我们看到其实是从右向左进行内存分配的即
0111 0010但严格来说分配的方式是标准未定义的即也有可能从左向右进行内存分配即
0101 1000总结 跟结构相比位段可以达到同样的效果但是可以很好的节省空间但是有跨平台的问题存在严格来说位段是不跨平台的 位段涉及很多不确定因素故注重可移植的程序应该避免使用位段。 4位段的应用
位段主要运用于数据在网络中的运输 PS这里仅简单说明一下位段的作用更多有关数据在网络中的运输等计算机网络的知识由于博主仍在学习这里就不做过多介绍啦。 数据在网络中运输时会在数据之上再封装数据以确保数据的准确运输。那么用来封装的数据肯定不能都像int等类型一样使用固定字节的大小。我们可以把网络想像为高速公路若全为大卡车则非常容易造成拥挤而通过位段可以到达 “缩小” 的作用从而减少流量的压力。 总结来说就是位段的使用有利于应对网络拥堵的问题。
二、枚举
顾名思义很好理解就是把某个事物所有可能的情况进行一一列举如掷骰子总共会出现六种情况一周总共有七天等等。
1. 枚举类型的定义
如上的两个例子作为枚举变量我们就可以定义为如下形式请看
enum Roll
{One,Two,Three,Four,Five,Six
};enum Day//星期
{Mon,Tues,Wed,Thur,Fri,Sat,Sun
};以上的enum Roll 和 enum Day就是枚举类型花括号中的数据就是枚举类型可能的取值称为枚举常量那么在定义一个枚举类型和枚举变量时有以下这么几个需要注意的点 1定义枚举类型时枚举常量间是使用逗号进行分隔的且最后一个枚举常量不需要加逗号 2枚举常量是有默认值的默认从0开始一次递增1也可以在定义的时候就进行赋初值如
enum Roll
{One 1,Two 2,Three 3,Four 4,Five 5,Six 6
};若只给其中一个枚举常量赋了初值那么该枚举常量前的枚举常量仍采用默认值而其后的枚举常量将以其为基准一次递增1如
enum Roll
{One,Two,Three 24,Four,Five,Six
};
其中枚举常量One和Two的值为01而枚举常量FourFiveSix的值分别为252627。 3和定义结构体变量相同若没有使用typedef关键字进行类型重命名那么在定义变量时就需要写全定义如用枚举类型定义一个名为Dice的枚举变量时正确的写法为enum Roll Dice;而不能写为Roll Dice; 4只能用枚举常量给枚举变量赋值也不能直接更改枚举常量的值因为是“常量”。
2. 枚举的使用
这里简单展现一下枚举的使用请看
enum Roll
{One 1,Two,Three,Four,Five,Six
};int main()
{enum Roll dice One;printf(Roll%d\n, dice);dice Six;printf(Roll%d\n, dice);return 0;
}
输出结果
3. 枚举的优点
从枚举的使用可以看出枚举的使用其实和#define定义常量非常类似那么相比之下我们使用枚举有什么优点呢 枚举的优点主要为以下几点 1增加代码的可读性和可维护性 2和#define定义的标识符相比枚举有类型检查更加严谨。 3一定程度上实现了封装防止了命名污染 4便于调试 5使用方便一次可以定义多个常量 额外补充一点其实#define定义的常量在编译期间就已经被替换为所定义的值了此时从整体代码的视角看来就会有些许的 “分裂” 感。 如
#define Max 100
int main()
{int m Max;return 0;
}上面代码中我们希望表达的是变量m中存着定义的最大值Max但代码经过编译后如上代码就变为了
#define Max 100
int main()
{int m 100; //Max直接替换为了100return 0;
}如此一来代码其实就不能很好地表达出我们所希望表达的意思了。
三、联合
1. 联合类型的定义
联合类型和结构体类型相似包含着一系列的成员但独有的特征是这些成员共用同一块内存空间故联合类型也被称为共用体或联合体 联合体可通过如下方式进行定义请看
//联合类型的定义
union Un
{char c;int i;
};//联合变量的定义
union Un un;
2. 联合的特点
开头提到了联合类型的特点其实就是联合体中的成员会共用同一块内存空间。 可以用这样一段简单的代码进行验证请看
union Un
{int i;char c1;
};int main()
{union Un un;printf(%p\n, (un.i));printf(%p\n, (un.c1));return 0;
}输出结果 可以看出联合体中的两个成员所占用的内存空间的地址是相同的也就是说两个成员共用同一块内存空间。
那么再请大家看一下下面代码会输出什么结果呢
union Un
{int i;char c1;
};int main()
{union Un un;un.i 0x11223344;printf(%x\n, un.i);un.c1 0x55;printf(%x\n, un.i);return 0;
}输出结果 解释 我们还是通过调试的方法来进行说明
初始状态 执行完语句un.i 0x11223344;后 由于成员i的类型为整型故内存中4个字节的内容被改为了44 33 22 11这里数据的顺序和打印出来的数据之所以反过来是因为当前机器的存储方式为小端存储在介绍位段时也已提到这里不再赘述啦。执行完语句un.c1 0x55;后 由于成员c1的类型为字符型故原内存中第一个字节的内容被改为了55。 故最后以十六进制输出就为11223355
在上面所提到的那篇介绍大小端的文章中给出了判断当前机器存储方式的一种方法这里根据联合类型的特点再提供一种判断方法请看
int check_sys() //大端返回0小端返回1
{union Un un;un.i 1;return(un.c 1);
}解释 若机器的存储方式为小端存储那么语句un.i 1;就会将内存中的内容改为01 00 00 00
低地址-----------------------高地址
//小端存储
01 00 00 00//大端存储
00 00 00 01那么此时成员c大小为一个字节中的内容也就为01故可直接对成员c中的值进行判断并返回判断结果即可。
那么对于一个联合体而言它所占用内存空间的大小究竟应该如何分配计算呢 请朋友们继续往下看。
3. 联合大小的计算
这里先放上联合体大小计算的规则 1联合体的大小至少为最大成员的大小 2联合体也是存在内存对齐的若最大成员的大小不是最大对齐数的整数倍时就要对齐到偏移量为最大对齐数的整数倍的地址处。 下面结合例子进行说明
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));程序输出结果 解释
对于联合体union Un1 成员char c[5]的大小为5个字节对齐数为1 成员int i的大小为4个字节对齐数为4 故最终联合体的大小需对齐到偏移量为最大对齐数也就是4的整数倍的地址处且需要保证联合体的大小至少为最大成员的大小故最终联合体的大小为8个字节对于联合体union Un2 成员short c[7]的大小为14个字节对齐数为2 成员int i的大小为4个字节对齐数为4 故最终联合体的大小需对齐到偏移量为最大对齐数也就是4的整数倍的地址处且需要保证联合体的大小至少为最大成员的大小故最终联合体的大小为16个字节
总结在计算联合体的大小时关键在于注意区分最大成员的大小与最大对齐数的概念前者除了关注类型还需关注个数而后者可只关注类型。
本章完。
看完觉得有觉得帮助的话不妨点赞收藏鼓励一下有疑问或有误地方的地方还恳请过路的朋友们留个评论多多指点谢谢朋友们