目录
1 结构体类型的声明
1.1 结构的声明
1.2 匿名结构的声明
2 结构体的自引用
3 结构体变量的定义与初始化
4 结构体内存对齐与大小计算
4.1 结构体的内存对齐规则
4.2 存在内存对齐的原因
4.3 默认对齐数的修改
5 结构体传参
说起C语言中的类型,我们可能都会想到char\short\int\double等,类似于这些整型、浮点型等被称为C语言的内置类型。而与此相对的就有自定义类型,顾名思义就是由我们自己定义并使用的类型。C语言中自定义类型有三种:结构体、枚举、联合,这里将要介绍的就是其中的结构体类型。
结构是一些值的集合,这些值称为成员变量,结构的每个成员可以是不同类型的变量:标量、数组、指针,甚至是其他结构体。
结构的声明主要包括三个部分:
①结构体关键字struct
②结构体标签(tag)
③结构成员
根据以上三个部分我们就可以实现一个结构的声明,如下所示:
struct tag{member-list; //成员变量}variable-list; //结构体变量,可根据需要决定是否在声明结构时就定义结构体变量
struct Stu{char name[20];//名字int age;//年龄char sex[5];//性别char id[20];//学号}; //分号不能丢
在声明结构时,也可以不完全声明,即声明时不加标签(tag),这样声明的结构也称为匿名结构体类型,如下所示:
//匿名结构体类型struct{int a;char b;float c;}x;struct{int a;char b;float c;}a[20], *p;
是否可以在一个结构中包含一个类型为该结构本身的成员呢?
struct Node{int data;struct Node next;};这里试着声明一个节点结构,里面还包含了一个节点结构体变量。一个数据类型都有其对应的大小,如果声明了这样一个结构,那它的大小该是多少呢?如图所示,假设一个结构体类型的大小是其中各成员变量的大小之和,那这里可以很清楚的知道int型变量的大小,但其中的结构体变量的大小又是多少呢?于是我们又去计算已身为结构成员的结构体变量中的结构成员的大小,就陷入了死循环,永远无法得到该结构体类型的大小究竟是多少。
接着再尝试运行一下这段声明,发现报出了下面的错误提醒:表示在结构中的结构体变量next未被定义。这是因为本身这里就是在声明一个新的结构体类型,只有声明完成了才可以使用其去定义一个该结构体变量,而在声明中又包含一个类型为该结构本身的成员变量,即是声明未完成就去使用了该结构体类型来定义变量,所以才报出了如下错误。
那是否表示结构体就不能自引用了呢?不是的,只是如上的自引用方式不正确,那该如何实现结构体的自引用?如下图所示,这里采用了链表的思想:如果在结构体中一部分成员用于储存数据,一部分成员用于指向所要引用的下一个结构体,即采用该结构的结构体指针存储下一个结构体的地址,这样对于储存数据的成员的大小是明确的,结构体指针的大小也是明确的(32位平台下为4个字节,64位平台下为8个字节),那该结构体的大小也就明确了。
正确的自引用:自己能够找到与自己相同的结构体成员
struct Node{int data; //数据struct Node* next; //指向下一个节点(结构体),存储地址};
或许有人会觉得每次都要使用struct Node这样一长串去定义变量太麻烦了,想能不能省略一个单词直接用struct去定义变量?这样是不行的,但C语言给我们提供了另一种方法:可以通过typedef进行类型重命名,如下,这样就可以将一种类型重命名为我们想要的名字。
typedef struct Node{int data;struct Node* next; //这里还不能使用重命名后的名字,因为此时重命名还未完成}Node;这里要注意的是:如果使用了typedef进行了重命名,就不能再在声明时就定义结构体变量了,不然会与新类型名造成冲突。
有了结构体类型后,我们就可以定义结构体变量了,其实上述已经提到了一种结构体变量的定义方法:在结构体类型声明时就在末尾定义所需的结构体变量;此外还有一种定义方法就是与使用int等类型定义变量一样使用声明好的结构体类型来定义变量。
方法①//声明点结构类型struct Point{int x;int y;}p1;方法②struct Point p2;
那么,该如何对结构体变量进行初始化呢?以下有几种初始化方法:
方法①:定义变量的同时赋初值
struct Point p3 = {1, 2}; //这里假设初始化点的坐标为(1,2)方法②:声明结构体类型的同时定义变量并初始化struct Node{int data;struct Node* next;}n1 = {10, NULL};方法③:结构体嵌套初始化struct Node{int data;struct Point p; //在结构体中定义其它结构体变量struct Node* next;}n1 = {10, {4,5}, NULL};struct Node n2 = {20, {5, 6}, NULL};方法④:结构体乱序初始化,通过.结构体成员的方式可以不按顺序进行初始化struct Node n3 = {.p = {5, 6}, .next = NULL, .data = 20};
结构体类型作为一种自定义的数据类型,同样可以通过sizeof()操作符来计算其大小,那结构体的大小是否是如上述提过的假设那样将结构体各成员的大小相加得到的呢?这里做个小测试:
声明如下结构体:
struct s1
{
char c1; //1字节
int i; //4字节
char c2; //1字节
};struct S2{char c1; //1字节char c2; //1字节int i; //4字节};
如果是按照成员变量大小相加计算的话,这里两个结构体大小的应该都是6字节。接下来用sizeof()进行计算如下:
通过测试发现:两个结构体的大小都不是6个字节,且即使两结构体的成员组成相同,但由于成员变量定义的先后不同,计算出的结构体大小也不同(即第一个结构体大小为12个字节,第二个结构体大小为8个字节)。那结构体类型的大小究竟是如何计算的呢?
变量创建后都会在内存中开辟一块空间,同样结构体变量创建后也会开辟一块属于自己的空间,这块空间会分配给每个结构体成员变量,要知道如何计算结构体大小,首先要知道这些结构体成员变量在内存中是如何存储。这里先要介绍一个宏:offsetof(type,number),包含在stddef.h头文件下,用来计算结构体成员相对于结构体变量起始位置的偏移量。接下来就来计算一下上面两个结构体中的成员的偏移量:
根据计算出的偏移量、结构体的大小以及成员变量的大小可以来分析结构体成员变量在内存中的存储情况,如下图所示:
存储char型变量需要1个字节的内存空间,int型变量需要4个字节的内存空间,但是如上图所示两结构体在内存中的存储情况表示:两结构体都造成了一定的空间浪费,尤其是声明的第一个结构体类型的浪费空间更多,那么计算机是按照什么规则来为结构体变量开辟内存空间的呢,又为什么要采用这种规则?下面我们就来解答这两个问题。
①第一个成员在相对于结构体变量起始位置偏移量为0的地址处
②其他成员变量要对齐到与结构体变量起始位置偏移量为对齐数的整数倍的地址处。
其中:对齐数 = 编译器默认的一个对齐数与该成员大小的较小值。VS中默认的值为8;Linux环境默认不设置对齐数,对齐数是结构体成员自身大小③结构体总大小为最大对齐数的整数倍(每个成员变量都有一个对齐数,其中最大的对齐数则为结构体的最大对齐数)
④如果嵌套了结构体的情况,嵌套的结构体对齐到自己的最大对齐数的整数倍处,结构体的整体大小就是所有对齐数中的最大对齐数(含嵌套结构体的对齐数)的整数倍。
根据以上对齐规则,现在可以再试着计算一下以下结构体的大小,如图:
通过sizeof(struct s3);与offsetof(type, member)进行测试:
结论:测试结构与根据规则计算出的结果一致。
根据大部分资料所述,存在内存对齐的原因主要有以下两点:
①平台原因(移植原因)
不是所有的硬件平台都能访问任意地址上的任意数据的;某些硬件平台只能在某些地址处取某些特定类型的数据,否则抛出硬件异常。
②性能原因
数据结构 ( 尤其是栈 ) 应该尽可能地在自然边界上对齐。若访问未对齐的内存,处理器需要作两次内存访问;而对齐的内存访问仅需要一次访问。(如下图所示)
总的来说,结构体的内存对齐是拿空间换取时间的做法。
在满足对齐的前提下,为了节省空间,我们可以设计结构体让占用空间小的成员尽量集中到一起。如上述的s1与s2,成员组成相同,但因为结构体s2中的成员集中定义在前面,所以其总体占用空间要更小。
结构体的对齐数影响其内存对齐,而每个成员的对齐数又与环境的默认对齐数有关,那能否通过修改默认对齐数来改变结构体的内存对齐呢?答案是肯定的,如下,这里我们通过#pragma这个预处理指令就可以修改及还原默认对齐数。
#pragma pack(1)//设置默认对齐数为1struct s2{char c1;int i;char c2;};#pragma pack()//取消设置的默认对齐数,还原为默认
这时再计算该结构体的大小,发现此时结构体的大小与其内存对齐方式都发生了改变:
总结:当结构在对齐方式不合适时,则可以修改默认对齐数来改变结构的对齐方式。
结构体传参主要有两种方式:
struct S{int data[1000];int num;};struct S s = {{1,2,3,4}, 1000};①结构体传参
void print1(struct S s){printf("%d\n", s.num);}②结构体地址传参void print2(struct S* ps){printf("%d\n", ps->num);}int main(){print1(s); //传结构体print2(&s); //传地址return 0;}
那哪一种传参方式更好一些呢?
了解过函数栈帧的创建与销毁后,可以知道函数形参是实参的一份临时拷贝,函数传参的时候,参数是需要压栈的,会有时间和空间上的系统开销。如果传递一个结构体对象,当结构体过大时,参数压栈的系统开销就比较大,会导致性能下降。因此在结构体传参的时候,建议采用结构体地址传参的方式。
以上是我对结构体类型的一些学习记录总结,如有错误,希望大家帮忙指正,也欢迎大家给予建议和讨论,谢谢!