目录
在之前的学习中,我们已对分支和循环、函数以及指针有了深刻的了解,但是对于生活中的一些对象例子我们难以仅仅使用上述学习的内容进行解决。
所以在本文,我们引入了一个新的概念,也是一个新的数据类型,即结构体数据类型。
下面我们需要先了解一下结构体:
结构体(struct)是由一系列具有相同类型或不同类型的数据构成的数据集合,也叫结构。
结构体和其他类型基础数据类型一样,例如int类型,char类型只不过结构体可以做成你想要的数据类型。以方便日后的使用。
在实际项目中,结构体是大量存在的。研发人员常使用结构体来封装一些属性来组成新的类型。由于C语言无法操作数据库,所以在项目中通过对结构体内部变量的操作将大量的数据存储在内存中,以完成对数据的存储和操作。
在实际问题中有时候我们需要几种数据类型一起来修饰某个变量。
对于接下来的内容,我们主要以结构体中的一些重点进行讲解,对于有关结构体的基础知识我们在这里不做过多的赘述。
我们在刚开始学习C语言的时候,想必大家都被要求动手去了解各个数据类型的大小。
像int, char,float,double,short等数据类型的大小是多少。
我们对此则使用sizeof进行打印,即:
- printf("%d", sizeof(int));
- printf("%d", sizeof(char));
- printf("%d", sizeof(float));
- printf("%d", sizeof(double));
- printf("%d", sizeof(short));
但如果,我们想要计算自己定义的一个结构体占多少个字节呢?
例:
- struct example
- {
- char a;
- char b;
- int i;
- }
如果我们想要知道此时结构体的大小,不妨使用sizeof来求出此时结构体的大小:
- struct example
- {
- char a;
- char b;
- int i;
- };
-
- int main()
- {
- printf("%d\n", sizeof(struct example));
- return 0;
- }
此时运算的结构为:
这里我们会好奇?
为什么会是8呢?
结构体中存在两个char类型和一个int类型,怎么算都应当是6个字节。
有疑问是很好的,说明你清楚数据类型在内存中占多少个字节。
接下来我们就来讲讲结构体在内存中的对齐操作,即结构体内存对齐。
关于偏移量:是我们建立结构体时里面每一个成员的位置相对于结构体首地址的距离的长度,单位是字节数。
在这里我们可以使用头文件
即:
由图不难看出各个成员的偏移量大小:0, 1, 4
此时a的存放地址就应当是从距离首地址0个字节处存放char类型的数据a,占1个字节。
此时b的存放地址就应当是从距离首地址1个字节处存放char类型的数据b,占1个字节。
此时c的存放地址就应当是从距离首地址4个字节处存放int类型的数据c,占4个字节。
如图所示:
所以此时结构体在内存中占8个字节。
我们在上面已经了解了关于偏移量的内容,接下来我们就对结构体的内存对齐方法进行剖析:
以下是方法:
1.第一个成员在结构体变量偏移量为0的地址处存放。
2.其他成员变要对齐到“对齐数”的整数倍的地址处,
对齐数 == 编译器默认的一个对齐数 与 该成员变量大小的较小值。
VS中默认的对齐数为8
Linux中没有默认的对齐数,对齐数就是成员自身的大小。
3.结构体总大小为最大对齐数(每个成员变量都有一个对齐数)的整数倍。
4.如果嵌套了结构体的情况,嵌套的结构体对齐到自己的最大对齐数的整数倍处,结构体的整体大小就是所有最大对齐数(含嵌套结构体的对齐数)的整数倍。
接下来我们举一个例子:
- struct S3
- {
- double d;
- char c;
- int i;
- };
-
- struct S4
- {
- char c1;
- struct S3;
- double d1;
- };
-
- int main()
- {
- printf("%d\n", sizeof(struct S4));
- return 0;
- }
如果你对此部分了解轻车熟路,那么看看答案就可以跳到下一部分了,
答案为:32字节
以下为讲解:
我们可以使用画图来解释此题目:
我们在按照以上方法,就可以得出此时S3此时的内存布局:
如此一来,我们就可以知道此时S4的内存布局:
所以32就为S4的大小。
至此我们解决了面试笔试题中常考的结构体内存对齐。
那么接下来我们来聊聊为什么要对齐?
不是所以的硬件平台都能访问任意地址上的任意数据的;
某些硬件平台只能在某些地址处取某些特定类型的数据,否则就会抛出硬件异常。
数据结构(尤其是栈)应尽可能地在自然边界上对齐。
原因在于,为了访问未对齐的内存,处理器需作两次内存访问;
而对齐的你村访问仅需要一次访问。
总体来说:结构体的内存对齐是拿空间换取时间的做法。
我们在上述详解了关于结构体内存对齐的方法以及原因,那么如果我们仅仅只是考虑到使用结构体而非计算大小的情况下,可能会将结构体作为参数来传递,那么在传参的时候我们应当采取哪些方式或者应当考虑到什么呢?
结构体传参如同于数据类型一样,存在两种传参方式,一种为值传递,另一种为址传递。
接下来我们来分别实现以下的结构体传参:
- //结构体传参
- struct S
- {
- int data[1000];
- int num;
- };
-
- void print1(struct S s)
- {
- printf("%d %d %d %d\n", s.data[0], s.data[1], s.data[2], s.num);
- }
-
- void print2(struct S* ps)
- {
- printf("%d %d %d %d\n", ps->data[0], ps->data[1], ps->data[2], ps->num);
- }
-
- int main()
- {
- struct S s = { {1,2,3},100 };
- print1(s);
- print2(&s);
- return 0;
- }
毫无疑问,以上两处代码没有问题,输出结果都为:
其实两种结构体传参的方法都是正确的,
但是我们在传递结构体作为参数时,我们首选“传址调用”
函数传参时,参数是需要压栈的,会有时间和空间上的系统开销。
如果传递一个结构体对象的时候,结构体过大,
参数压栈的系统开销比较大,所以会导致性能的大幅度下降。
因此我们首选“传址调用”!
位段,C语言允许在一个结构体中以位为单位来指定其成员所占内存长度,这种以位为单位的成员称为“位段”或称“位域”( bit field) 。利用位段能够用较少的位数存储数据。
信息的存取一般以字节为单位。实际上,有时存储一个信息不必用一个或多个字节,例如,“真”或“假”用0或1表示,只需1位即可。在计算机用于过程控制、参数检测或数据通信领域时,控制信息往往只占一个字节中的一个或几个二进制位,常常在一个字节中放几个信息。
总的来说,位段的出现就是为了节省空间。
在C99之后,位段中的数据类型一般都为int,char类型。
位段的格式就是成员名后面有一个冒号和一个数字。
例:
- struct A
- {
- int _a : 2;//"_a"占用的2个bit位的空间
- int _b : 5;//"_b"占用的5个bit位的空间
- int _c : 10;//"_c"占用的10个bit位的空间
- int _d : 30;//"_d"占用的30个bit位的空间
- };
1.位段的成员市int,unsigned int,signed int,int或者char类型(属于整形家族)
2.位段的空间上市按照需要以4个字节(int)或者1个字节(char)的方式来开辟的。
3.位段涉及许多不确定因素,位段是不跨平台的
注重于移植的程序应避免使用位段
接下来我们来列举一个例子让我们好好的认识下位段。
- struct S
- {
- char a : 3;
- char b : 4;
- char c : 5;
- char d : 4;
- };
-
- int main()
- {
- struct S s = { 0 };
- s.a = 10;
- s.b = 12;
- s.c = 3;
- s.d = 4;
- return 0;
- }
我们想要知道此时在内存中,s存储的是哪些值
此时可以通过画图来进行讲解:
此时位段在内存中开辟的空间如图所示。
我们在这里假设的是从低位——>高位。
那么对于位段占各成员所占的bit位,我们也给出了图解:
我们将红色框里面的二进制位存放到内存中,则内存应如图所示:
那么在内存中,如果为假设的那样,从低位——>高位。
则应当为 62 3 4
我们到监视窗口观察内存可得:
所以假设成立,位段的基本内容已经实现。
接下来我们再来扩展扩展位段。
1.int的位段被当成有符号数还是无符号数是不确定的。
2.位段中最大位是不能确定的。(16位机器最大为16,32机器最大为32,若写成27,
则会在16位机器上出现问题)
3.位段中成员在内存中从左向右分配,还是从右向左分配尚未定义。
4.当一个结构体包含2个位段,第二哥位段成员比较大时,
无法容纳与第一个位段剩余的位时,是否舍弃还是利用,我们在这里是不确定的。
在数学和计算机科学理论中,一个集的枚举是列出某些有穷序列集的所有成员的程序,或者是一种特定类型对象的计数。这两种类型经常(但不总是)重叠。
我们在之前的学习中,我们就知道关于常量的定义有:
字面常量,
#define定义的标识符常量,
const 修饰的常变量,
枚举常量。
下面我们实现一个最最简单的枚举例子:
- enum sex
- {
- MALE,//枚举常量,不可以在main函数里进行修改
- FEMALE,
- SECRET
- };
-
- int main()
- {
- printf("%d\n", MALE);
- printf("%d\n", FEMALE);
- printf("%d\n", SECRET);
- return 0;
- }
输出结果为:
下面我们不对枚举做过多的赘述,其含金量我们需要用代码积累才可得知。
我们在以后实现通讯录打印菜单时则可以使用枚举。
联合是一种特殊的自定义类型。
这种类型定义的变量也包含一系列程艳,特征是这些成员共用同一块空间(所以联合也叫共用体)
下面我们来测试一下,联合体的地址内存信息:
- union Un
- {
- char c;
- int i;
- };
-
- int main()
- {
- union Un un;
- printf("%p\n", &un);
- printf("%p\n",&(un.c));
- printf("%p\n", &(un.i));
- return 0;
- }
此时我们看看输出结果:
我们发现此时地址一样,那么在内存中联合体的内存布局应为:
我们现在对联合体在内存中的存放已经有了深刻的了解,那如果我们想知道32位机器中的大小端呢?
我们有以下两种方法判断:
方法一:
- //方法一
- int Check_system(int* a)
- {
- return *((char*)a);
- }
-
- int main()
- {
- int a = 1;
- int ret = Check_system(&a);
- if (ret == 0)
- {
- printf("大端\n");
- }
- else
- {
- printf("小端\n");
- }
- return 0;
- }
此方法运用的是强制类型转换为char*指针,并进行解引用,此时就可以访问到a的第一个字节。
方法二:
- //方法二:
- int Check_system()
- {
- union
- {
- char c;
- int i;
- }u;
- u.i = 1;
- return u.c;
- }
-
- int main()
- {
- int ret = Check_system();
- if (ret == 1)
- {
- printf("小端\n");
- }
- else
- {
- printf("大端\n");
- }
- return 0;
- }
此方法则运用到了联合体,通过上述讲解的内存布局不难得出结论。
1.联合体得大小至少是最大成员的大小。
2.当最大成员大小不是最大对齐数的整数倍的时候,就要对齐到最大对齐数的整数倍。
- //联合体大小计算
- union Un1
- {
- char c[5];
- int i;
- };
-
- union Un2
- {
- short c[7];
- int i;
- };
-
- int main()
- {
- printf("%d\n", sizeof(union Un1));
- printf("%d\n", sizeof(union Un2));
- return 0;
- }
输出结果为:
总结:
以上我们介绍了自定义数据类型的结构体变量,其中包含内存对齐,位段,枚举和联合体。
我们下来应当多看看这篇文章,并且下来多练习练习。
记住
坐而言不如起而行
Action speak louder than words!
光说不练,假把式!
最后祝大家中秋快乐,国庆快乐!!!
可以在下面访问我的Gitee:
The_Struct_CSDN/The_Struct_CSDN/test.c · 无双/test_c_with_X1 - Gitee.com