1.结构体
1.1结构的基本知识
结构是一些值的集合,这些值被称为成员变量。结构的每个成员可以是不同类型的变量。
我们前面说数组是一组相同类型元素的集合,而结构体中的每个成员可以使不同类型的变量。
1.2结构体的声明
例如,描述一个学生:
struct Stu { char name[20];//名字 int age;//年龄 char sex[5];//性别 char id[20];//学号 };//分号不能丢
我们也可以创建结构体变量s1,s2,s3:
法一:
struct Stu { char name[20];//名字 int age;//年龄 char sex[5];//性别 char id[20];//学号 }s1,s2,s3;//分号不能丢
法二:
#include<stdio.h> struct Stu { char name[20];//名字 int age;//年龄 char sex[5];//性别 char id[20];//学号 };//分号不能丢 int main() { struct Stu s1, s2, s3; return 0; }
1.3特殊的声明
在声明结构体时,可以不完全的声明,这种也可以叫匿名结构体类型。
如下所示:
struct { int a; char c; float f; }s1,s2;
在声明结构体时,结构体类型名被省略了,注意此时要定义一个结构体变量只能使用法一的方法,不能用法二的方法。
下面我们再来创建一个和上面一样的匿名结构体类型,并将它定义为指针,那编译器会不会认为这两个结构体类型是一样的呢?
#include<stdio.h> struct { int a; char c; float f; }x; struct { int a; char c; float f; }*p; int main() { p = &x; return 0; }
运行:
可以看到,编译器报错了,两种类型不兼容,所以说,虽然我们声明的结构体类型看似相同,但是编译器不认为它们是一种类型。所以一般情况下,匿名结构体只能使用一次。
1.4结构的自引用
在这之前,我们先来了解一些数据结构的知识。
顺序表是顺序存储的,链表是通过节点1找到节点2,然后根据节点2找到节点3.......,那要怎样实现链表呢?
有人说,只要通过这个节点能找到下一个节点就行了,那我们就把自己包含在自己里面就能一直找下去啊,这种做法行吗?
如下面代码所示:
struct Node { int data; struct Node next; };
显然是不行的,大家考虑一下,如果使用上述方法,sizeof(struct Node)该怎么计算?根本无法计算,因为第一个next里面包含data和next,下一个这个next又包含data和next,这样一直下去,结构体类型的大小根本计算不了,所以这种方法行不通。
其实这里使用结构体指针就能解决了,只要把节点2的地址存放在节点1里面,节点3的地址存放在节点2里面........就能根据地址找到后续的节点。
struct Node { int data;//数据域 struct Node* next;//指针域 };
这就叫做结构体的自引用。
下面我们再来思考一个问题:
下列对匿名结构体类型进行重命名,然后在结构体内使用它重命名后的类型名,这种方式正确吗?
typedef struct { int data; Node* next; }Node; int main() { Node n = { 0 }; return 0; }
这种方式是不对的,因为我们在对匿名结构体进行重命名为Node之前,这个结构体类型应该是已经存在的,但是我们还没有命名为Node呢,它在结构体内部就提前使用了Node*,这明显就不对,就像是先有蛋还是先有鸡的问题。
正确的写法应该是这种,不要用匿名结构体类型:
typedef struct Node { int data; struct Node* next; }Node; int main() { Node n = { 0 }; return 0; }
1.5结构体变量的定义和初始化
结构体的定义:上文中也讲过
#include<stdio.h> struct SN { char c; int i; }sn1,sn2;//全局变量 int main() { struct SN sn3, sn4;//局部变量 return 0; }
结构体变量的初始化:
#include<stdio.h> struct SN { char c; int i; }sn1 = { 'q',100 }, sn2 = {.i=200,.c='w'};//全局变量 int main() { //struct SN sn3, sn4;//局部变量 printf("%c %d", sn2.c, sn2.i); return 0; }
上述代码对sn1,sn2进行初始化及打印。
运行结果:
当然,我们的结构体内部也可以出现结构体变量:
#include<stdio.h> struct SN { char c; int i; }sn1 = { 'q',100 }, sn2 = {.i=200,.c='w'};//全局变量 struct S { double d; struct SN sn; int arr[10]; }; int main() { struct S s = { 3.14,{'w',10},{1,2,3} }; printf("%lf %c %d\n", s.d, s.sn.c, s.sn.i); int i = 0; for (i = 0; i < 10; i++) { printf("%d ", s.arr[i]); } return 0; }
运行结果:
1.6结构体内存对齐
我们现在已经掌握了结构体的基本使用,现在我们来深入讨论一个问题:计算结构体的大小。
这也是一个热门考点:结构体内存对齐。
先来看下面这段代码:
#define _CRT_SECURE_NO_WARNINGS 1 #include<stdio.h> struct s1 { char c1; int i; char c2; }; struct s2 { int i; char c1; char c2; }; int main() { printf("%zd\n", sizeof(struct s1)); printf("%zd\n", sizeof(struct s2)); return 0; }
大家觉得结果是什么呢?
直接来看吧:
是不是和预想的结果差距很大,那这又是为什么呢?
这就不得不提一下结构体的对齐规则了:
1.第一个成员变量在与结构体变量偏移量为0的地址处。
2.其他成员变量要对齐到某个数字(对齐数)的整数倍的地址处。
对齐数 = 编译器默认的一个对齐数与该成员自身大小之间的较小值。
VS中对齐数默认为8
Linux中没有默认对齐数,对齐数就是成员自身的大小
3.结构体总大小为最大对齐数(每个成员变量都有一个对齐数)的整数倍。
4.如果嵌套了结构体的情况,嵌套的结构体对齐到自己的最大对齐数的整数倍处,结构体的整体大小就是所有最大对齐数(含嵌套结构体的对齐数)的整数倍。
了解了结构体对齐规则,我们来分析一下上述代码:
先来看s1,根据第一条规则,第一个成员char c1放在相对于结构体变量偏移量为0的地址处,
第二个成员int i根据第二条规则,要对齐到对齐数的整数倍的地址处,VS默认的对齐数是8,i自身的大小是4,所以对齐数应该是4,所以i应该从地址为4的位置往后占用4个字节,
第三个成员char c2,它的对齐数是1,而任何数都是1的整数倍,所以接着上一个成员地址往下存一个字节,
现在三个成员占用到9个字节的空间,而根据规则3,结构体总大小是最大对齐数的整数倍,c1的对齐数是1,i的对齐数是4,c2的对齐数是1,所以最大对齐数是4,9显然不是4的整数倍,所以要往后浪费3个字节的空间到12,所以最终打印结果是12。
我们来看结构体struct s1在内存中的存储:
同理,结构体struct s2也是参考上述对齐规则:
下面我们再来看一个例题:
#include<stdio.h> struct s3 { double d; char c; int i; }; int main() { printf("%zd\n", sizeof(struct s3)); return 0; }
自己试着做一下
下图是答案:
以上是对对齐规则前3个的使用,下面我们来看看,对齐规则第4个规则该怎么使用:
4.如果嵌套了结构体的情况,嵌套的结构体对齐到自己的最大对齐数的整数倍处,结构体的整体大小就是所有最大对齐数(含嵌套结构体的对齐数)的整数倍。
如果我们在结构体struct s4中嵌套一个结构体struct s3,那struct s4的大小是多少?
#include<stdio.h> struct s3 { double d; char c; int i; }; struct s4 { char c1; struct s3 s3; double d; }; int main() { printf("%zd\n", sizeof(struct s4)); return 0; }
结果应该是:32
首先,第一个成员c1存储在偏移量为0的地址,而第二个成员s3是个结构体,按照规则4,嵌套的结构体对齐到自己的最大对齐数的整数倍处,上文中struct s3中的最大对齐数是8,那s3就应该从地址8往后占用16个字节,到地址23,24恰好是第三个成员d的对齐数8的整数倍,所以d应该从地址24往后占用8个字节空间,此时结构体一共占用32个字节的空间,32恰好是所有对齐数中最大对齐数8的整数倍,所以结构体最终的大小就是32。
那什么存在内存对齐呢?
1.平台原因(移植原因):
不是所有的硬件平台都能访问任意地址上的任意数据的;某些硬件平台只能在某些地址处去某些特定类型的数据,否则会抛出硬件异常。
2.性能原因:
数据结构(尤其是栈)应该尽可能的在自然边界上对齐。
原因在于,为了访问未对齐的内存,处理器需要作两次内存访问;而对齐的内存仅需要一次访问。
例如:我们要存一个char型和一个int 型的数据,(假设是32位机器,一次读取4个字节),如果没有对齐,那我们从char开始读,第一次只能读到int型的3个字节,还得读一次才能凑够4个字节:
如果对齐了,char型在存储时会浪费掉3个字节,然后再存储int型数据,这样我们只需读一次就能得到int型的数据:
总的来说,结构体的对齐是拿空间换取时间的做法。
那在设计结构体的时候,我们既要满足对齐,又要节省空间,如何做到?
这个可以看看我们之前s1和s2,
struct s1 { char c1; int i; char c2; }; struct s2 { int i; char c1; char c2; };
成员相同,写法不同,结构体的大小不同,分别是12和8,由此可以总结出,在设计结构体的时候,要想既满足对齐,又要节省空间,把占用空间小的成员尽量写在一起即可。
1.7修改默认对齐数
之前我们见过了#pragma这个预处理命令,这里我们可以用它来修改默认对齐数:
#include<stdio.h> #pragma pack(1)//修改默认对齐数为1 struct s1 { char c1; int i; char c2; }; #pragma pack()//取消修改的默认对齐数,还原为默认 int main() { printf("%zd\n", sizeof(struct s1)); return 0; }
修改默认对齐数为1后,结构体成员的存储应该是挨着存储的,那打印结果就是6。
一般我们在设置默认对齐数时,设置为2的次方数。
1.8结构体传参
这个我们在之前的章节中讲过,这里不做细讲,直接看代码:
#define _CRT_SECURE_NO_WARNINGS 1 #include<stdio.h> struct S { int arr[100]; int num; }; //结构体传参 print1(struct S s) { printf("%d\n", s.num); } //结构体地址传参 print2(struct S* ps) { printf("%d\n", ps->num); } int main() { struct S s = { {1,2,3},100 }; print1(s);//传结构体 print2(&s);//传地址 return 0; }
上面有两种结构体传参方式,我们选择的那种方式比较好呢?
答案是第二种,结构体地址传参。
原因:
函数传参的时候,参数是需要压栈,会有时间和空间上的系统开销。
如果传递一个结构体对象的时候,结构体过大,参数压栈的的系统开销比较大,所以会导致性能的下降。
形参是是实参的一份临时拷贝,要是选择结构体传参的话,需要额外开辟一处空间,会造成空间的浪费。