一、结构体基础知识
1.虽然今天弄了一个目录,但是开头必须还是我的大学生活,今天是军训第3天,双腿酸痛,军训还是非常磨砺人的,每天5点50起床(搞得我现在眼睛都要睁不开了),所以为了早睡,现在步入正题
2.首先我们讲一下如何声明结构体,如何初始化结构体和什么是结构体变量的成员变量,以一个代码为例:
#include<stdio.h> struct people //这个就是声明结构体的类型(特别要注意这个是一个类型(类似于int类型的数据),所以在使用时就应该把整个结构体类型用上) { //下面这些就是我的结构体变量的成员变量; char name[20]; char tele[12]; int age; double num; }s1;//这个是一个全局变量,是在我struct 结构体类型中创建的一个全局变量(且这个位置要注意什么是全局变量什么是局部变量(在main函数内部的就是局部变量,在main函数外的就是全局变量,所以此时这个s1就是一个全局变量)) 所以此时我就可以拿这个结构体类型来创建一个结构体变量
创建一个结构体变量,代码如下:
int main() { struct people s1;//这个就是利用结构体类型创建一个结构体变量 struct people s1 = { "校园网","10086",18,3.14};//这个就是结构体类型的初始化,要用大括号进行 printf("%s %s %d %lf", s1.name, s1.tele, s1.age, s1.num);//这个就是结构体成员的访问,用操作符 . return 0; }
二、结构体的进阶(有关结构体的自引用,嵌套,内存对齐和内存设计)
(一、)首先是结构体的嵌套
1.现在我们讲一下结构体的进阶,比如结构体中含有结构体的使用和关系,首先看一个结构体中含有结构体的代码:
#include<stdio.h> struct people//切记这个东西是结构体类型,不是变量,不能乱用,想要用的话,就一定要创建一个变量出来 例:后面的 p 就是我可以用的一个结构体变量 { char name[20]; char ch; int age; }; struct S//切记这个东西是结构体类型,不是变量,不能乱用,想要用的话,就一定要创建一个变量出来 例:下面的 s 就是我的可以用的一个结构体变量 { char sex[10]; double d; struct people p; }; int main() { struct S s = { "男",3.14,{"我是小白",'W',18 } };//这个还是那个道理(对结构体的初始化,但是因为我要初始化两个结构体,所以在 {} 中,我还要再写一个 {} ,这样我才可以把两个结构体都给初始化) printf("%s %lf %s %c %d", s.sex, s.d, s.p.name, s.p.ch, s.p.age);//这边的对结构体成员变量的访问和上述说的是一个道理的,不过只是二重访问,因为是结构体中包含结构体,所以我要二重访问 return 0; } 以上这个就是结构体中含有结构体的代码
(二、)结构体的自引用
2.第二点进阶,我们来讲一下什么是结构体的自引用,看代码如下:
struct Node { int data; struct Node* next;//这个与数据结构有一点关系,意思就是struct Node这个数据结构类型的位置的指针,此时有了这个指针我就可以找到下一个数据位置,这个就叫链表,(并且这个就是结构体的自引用(结构体的自引用:就是找一个与这个结构体类型相同的结构体,我用一个指针变量去指向这个结构体的数据)) }; int main() { struct Node n ={0};//在main函数内部,我只能定义局部变量,想要定义全局变量,我就一定要在main函数外部定义,例:s1; return 0; } 这个可以附上一个与数据结构有关的图(所以这个结构体的自引用就是与数据结构有一定的关系)
就是当我将我的数据(例:1 2 3 4 5)放在我的内存中时
此时的数据在内存中是随机分布的,所以如果想要重新按照顺序把它们找出来,我们就要用一个叫(链表的东西),让我的数据此时不仅有数据域,还有一个指针域(只有有了指针,我才有可能按照顺序找到它们),所以如下图:
所以这个就是结构体的自引用的作用,我可以用自己结构体中创建的指针去指向我自己结构体中的数据,这样我就可以很好的找回数据;(且这边再一次起强调,指针的大小只跟电脑是32位的还是64位的有关,如果是32位,则指针的内存大小就只能是4,是64位内存大小就只能是8)
(三、)结构体的内存对齐(如何计算结构体的所占内存大小)
一、如果我们想要计算一个结构体所占内存空间的大小,首先我们就要掌握结构体的对齐规则
1.第一个成员与结构体变量的偏移量为0的地址处;
2.其它成员变量对齐某个数字(对齐数)的整数倍地址;
3.并且结构体的总大小为最大对齐数的整数倍(并且每一个变量都有一个对齐数),在vs编译器下,对齐数默认是8;并且对齐数指的是:一个对齐数与该成员大小的较小值
4.当结构体在嵌套使用时,嵌套的结构体对齐到自己的最大对齐数的整数倍的地址处,结构体的总大小就是所有最大对齐数的整数倍(含嵌套结构体中的最大对齐数);
看完这4点,人都傻了,看不懂什么意思,所以现在进行总结:
1.首先举一个例子:有char a; 按照上述对齐的的概念,我就还可以知道此时char a 的对齐数是 (1)
所以同理 int name;此时的对齐数是(4)
2.所以按照上述所说此时的 int name;只有在内存中遇到4的倍数的时候,此时才可以进行存放我的地址(如果地址是1 2 3 的时候就不能存放,所以此时这个1 2 3 地址处的空间就会被略过,就是里什么都不能放,也就是内存的浪费),如果想char a 这种对齐数为(1)则在内存中的任意位置都可以进行存放
3.所以得出结论:默认成员的大小其实就是我的对齐数(我的对齐数的整数倍就是我这个变量能够存放地址的位置空间)
4.所以下面让我们用一个题目,来进一步认识什么是内存对齐,代码如下:
#include<stdio.h> struct S1 { char ch1; int a; char ch2; }; struct S2 { char ch1; char ch2; int a; }; int main() { struct S1 s1 = { 0 }; printf("%d\n", sizeof(s1)); struct S2 s2 = { 0 }; printf("%d", sizeof(s2)); return 0; }
输出结构如图:
这里应该大家就非常不理解,为什么是这个答案了,所以这里就用图的方式给大家讲一下(具体原理就是上面所说的内存对齐),如图:
所以上图就是为什么struct S1的答案是12的原因,并且struct S2原理同上,这个就是对齐规则的使用和结构体占内存大小的计算(就是使用了内存对齐的方式进行数据的储存)
二、我们讲一讲为什么要进行内存的对齐
1.就是平台的原因:并不是所有的硬件平台都能访问任意地址上的任意数据,某些硬件平台只能在某些地址处取得某些特定类型的数据
2.性能的原因:数据结构(尤其是栈),应该尽可能地在边界上对齐,原因在于,为了访问未对齐的内存,处理器要进行两次访问才可以读取那个内存的数据,而对齐了的内存就只要访问一次,具体原因如图所示:
这个就是为什么不进行内存对齐我的处理器需要访问两次内存的原因;所以假如我进行了内存对齐把char和int中间多余的3个字节的空间省略,实现了内存的对齐,那么此时我的处理器就只需要访问一次内存了(这样就可以节省我处理器的时间,使处理器运行更加的快)
3.总结就是:拿空间换时间
(四、)如何修改默认对齐数
1.如代码所示:
#pragma pack(1)//设置默认对齐数为1 #pragma pack(4)//设置默认对齐数是4 struct S { char ch; int i; }; #pragma pack()//取消默认对齐数的设置
三、offsetof的意思
1.首先offsetof是一个宏的定义,意思就是什么什么的偏移量,使用方法如代码和图所示:
#include<stddef.h> #include<stdio.h> struct S1 { char ch1; int a; char ch2; }; struct S2 { char ch1; char ch2; int a; }; int main() { printf("%d\n",offsetof(struct S1,a)); printf("%d\n",offsetof(struct S2,a)); return 0; }
答案如下图:
使用的规则如图:
四、结构体的传参
1.看到传参两个字,我们首先应该想到的就是(传值和传址两种类型),如下代码,我们一起看一下结构体是如何进行传参的
#include<stdio.h> struct S { int a; char ch; double b; }; void Init1(struct S tmp)//因为此时要改变外部的数据,所以这里用传值不好 { tmp.a = 100; tmp.ch = 'W'; tmp.b = 3.14; } void Init2(struct S* ps) { ps->a = 100; ps->b = 3.14; ps->ch = 'W'; } Print1(struct S tmp)//因为此时只是进行打印,所以可以用传值的方法 { printf("%d %c %lf\n",tmp.a,tmp.ch,tmp.b); } Print2(struct S* ps)//这边因为是用传地址的方法,所以可以写成Print2(const struct S tmp);这样就可以防止我外部的数据被改变,更加安全 { printf("%d %c %lf\n", ps->a, (*ps).ch, ps->b);//ps->a, (*ps).ch这两种写法是一样的道理 } int main() { struct S s = { 0 }; Init1(s);//这个就是结构体的传值调用 Init2(&s);//这个就是结构体的传址调用 Print1(s); Print2(&s); return 0; }
这个代码就可以清晰的看出我的结构体是如何进行传参的,和如何进行传参后的使用
五、位段的使用和注意
1.首先位段就是一个二进制位是一个比特位,且位段的成员必须是int、unsigned int、signed int
2.位段的成员后有一个冒号和数字(且注意这个数字是不能超过32的)
3.位段的使用就是为了节省我的空间
4.例:代码所示
#include<stdio.h> struct A { int _a : 2;//写成int a : 2;也可以,一个意思 int _b : 5;//这个数字代表的就是比特位 int _c : 10; int _d : 30; }; int main() { struct S s={0}; printf("%d\n",sizeof(A)); return 0; }
5.注意点就是在于这个比特位到底是怎样计算的和位段是如何进行空间的节省的,原因就是:当我的位段前面的类型是int类型时,那么内存每次就会开辟32和比特位,如果是char类型是内存每次就开辟8个比特位,具体原理如图:
所以这个就是位段的使用
总结:
结构体和数据的使用息息相关,内存的理解至关重要!