目录
一、结构的声明
1.1 一般声明
1.2 特殊声明
1.3 结构体自引用
二、结构体变量的定义和初始化
2.1 结构体变量的定义
2.2 结构体变量的初始化
三、结构体内存对齐
3.1 代码分析
3.2 结构体内存对齐的规则
3.3 嵌套结构体的大小
3.4 存在结构体内存对齐的原因
3.5修改默认对齐数
四、结构体传参
结构是一些值的集合,这些值称为成员变量,结构的每个成员可以是不同类型的变量。这里小编将带着大家一起来了解学习结构体,分别从结构体的声明、自引用,结构体的定义和初始化,以及结构体内存对齐和结构体传参这些方面来学习结构体。
struct tag
{
member-list;
}variable-list;
结构体的语法形式如上代码,struct是关键字,tag是个标签名,这个是可以自定义的,根据自己实际情况来进行定义的,紧跟着是大括号,里面是成员列表,最后大括号外面是变量列表。举个例子吧,用结构体来描述学生的相关信息。
struct Stu
{
char name[20];//名字
int age;//年龄
char sex[5];//性别
char id[20];//学号
}s1,s2,s3; //分号不能丢
这里的name、age、sex和id都是成员列表。s1、s2、s3是结构体变量,当然也可以在下面重新命名,例如:
struct Stu
{
char name[20];//名字
int age;//年龄
char sex[5];//性别
char id[20];//学号
}; //分号不能丢
int main()
{
struct Stu s1,s2,s3;
return 0;
}
在声明结构体的时候,可以不完全声明。
例如:
struct
{
int a;
char b;
float c;
}x;
int main()
{
return 0;
}
如上代码,struct后面没有标签名,只是在整个结构体后面直接命名了一个x作为结构体的变量名,这种就属于匿名结构体类型,其实这种匿名结构体类型一般情况只能用一次,在后面代码中不能再次创建结构体了。
在此基础上,我们在创建一个成员相同结构体,以结构体指针的形式命名,如下代码,
struct
{
int a;
char b;
float c;
}x;
struct
{
int a;
char b;
float c;
}* p;
int main()
{
p = &x;//错误写法
return 0;
}
此时我们会认为,这两个结构体成员一模一样,然后我们用p来接收x的地址,其实这是错误的。
注意:这样编译器会发出警告,警告说p的类型和&x的类型不兼容,因为我们这两个结构体连标签都没有,编译器会认为这是两种不同结构体。这种匿名结构体类型绝大部分情况下我们不会去使用。
结构体的自引用就类似数据结构中的链表,为了找到下个节点,在自己的成员列表中记录自己同类型的结构体的指针作为节点。
(注意:必须是下个结构体的指针(地址),不能存放下个结构体的变量名)
struct Node
{
int data;//数据域
struct Node* Next;//指针域
};
struct Point
{
int x;
int y;
}p1; //声明类型的同时定义变量p1
struct Point p2; //定义结构体变量p2
定义结构体变量有两种方法:
①声明类型的同时定义变量
②重新定义结构体变量
struct Stu
{
char ch;
int a;
}s1={.a=100,.ch='x'},s2={.a=500,.ch='f'};
//s1={'w',100},s2={'s',200};
如上这种声明类型和定义结构体变量的同时初始哈结构体成员变量,我们可以使用' . '操作符来进行赋值,这样可以不按照顺序来赋值,也可以直接进行赋值初始化,但需要按照顺序。
当然结构体里面也可以包含其他的结构,初始化也是同理。代码如下:
struct Stu
{
char ch;
int a;
}s1, s2;
struct School
{
float d;
struct Stu s1;
int x;
int arr[3];
};
int main()
{
struct School Sn = { 3.14,{'m',666},999,{4,5,6} };
return 0;
}
这时我们以及掌握结构体的基本使用了,那么接下来我们来深入探讨一下结构体的一个很重要的问题,计算结构体大小,这是一个很热门的考点(结构体内存对齐)。
我们先来看一个练习吧,
struct S1
{
char c1;
int i;
char c2;
};
struct S2
{
char c1;
char c2;
int i;
};
int main()
{
printf("%d\n", sizeof(struct S1));
printf("%d\n", sizeof(struct S2));
return 0;
}
当我们看到上面的题目时候,感觉这两个结构体的是一样的,就只是成员变量顺序换了一下罢了,我们会以为这两个结构体的大小是一样大的,char是一个字节,int是四个字节,所以会以为这两个结构体的大小都是6字节,当我们运行代码之后,我们发现事实却不是这样的。
第一个结构体的大小是12字节,第二个结构体的大小是8字节,为什么是这样的呢,这里就涉及到结构体内存对齐的问题。
这里先介绍一个宏,叫做 offsetof ,这个宏可以计算结构体某个成员相较于结构体起始位置的偏移量。(如下图)
两个参数,第一个参数是结构体类型,第二个参数结构体成员,头文件
代码如下:
#include
struct S1
{
char c1;
int i;
char c2;
};
int main()
{
printf("%d\n", offsetof(struct S1, c1));
printf("%d\n", offsetof(struct S1, i));
printf("%d\n", offsetof(struct S1, c2));
return 0;
}
我们发现,c1距离结构体起始位置的偏移量是0,i距离结构体起始位置的偏移量是4,c2距离结构体起始位置的偏移量是8,如下图所示
但是这样算下来,应该是9个字节呀,为什么显示的是12个字节呢?其实本质上,在最后又浪费了3个字节,使得最后是12个字节。接下来,我们来研究一下为什么是这样呢?
① 第一个成员在与结构体变量偏移量为0的地址处。
② 其他成员变量要对齐到某个数字(对齐数)的整数倍的地址处。
对齐数 = 编译器默认的一个对齐数 与 该成员大小的较小值。
VS中默认的值为8
Linux中没有默认对齐数,对齐数就是成员自身的大小
③ 结构体总大小为最大对齐数(每个成员变量都有一个对齐数)的整数倍。
④ 如果嵌套了结构体的情况,嵌套的结构体对齐到自己的最大对齐数的整数倍处,结构体的整体大小就是所有最大对齐数(含嵌套结构体的对齐数)的整数倍。
利用这些规则,来解读一下上面的疑惑,第一个成员c1放在了结构体的第一个空间的位置,也就是与结构体偏移量为0的地址,然后从第二个成员开始,往后每一个成员都要对齐到某个对齐数的整数倍处。这里小编使用的是VS,默认对齐数是8,针对我们这个代码来说,第二个成员变量i的自身大小为4,编译器的默认对齐数是8,所以i的对齐数为4,所以所放的位置应该对齐到4的倍数处,此时会发现前面有3个字节的空间浪费掉了,再看第三个成员变量,通过计算c2的对齐数1,这时直接向后放就可以了,那么这里已经使用9个字节了,怎么算的12呢,这时就要利用第三个规则了,第一个变量对齐数是1,第二个变量的对齐数是4,第三个变量的对齐数1,所以最大对齐数是4,结构体总大小为最大对齐数的整数倍,由此可以得出,结构体的总大小为12。如下图所示,
在上面,我们把结构体在内存对齐的规则讲完后,就把最开始的几个代码和疑惑全部解释清楚了,但是规则4我们还没有利用到,这里小编再聚一个例子来解读一下规则4.
struct S3
{
double d;
char c;
int i;
};
struct S4
{
char c1;
struct S3 s3;
double e;
};
看到上面代码,通过规则我们可以轻易的算出s3结构体的大小为16字节。那么结构体s4的大小为多少呢,首先c1占用第一个字节,然后成员s3,s3的最大对齐数(s3自己所有成员的对齐数的最大值)为8,自身大小为16,所以对齐数为8,所以s3要放在8的整数倍处,然后在放e,计算完发现此时使用了32个字节,最后结构体的总大小为最大对齐数,也就是8的倍数,发现32刚好是32的倍数,所以最后结构体s4的大小为32。
好了,讲到这里,结构体内存对齐的规则已经讲清楚了,但是,为什么会存在结构体对齐这个东西呢?
大部分资料是这样说的。
①平台原因(移植原因):
不是所有的硬件平台都能访问任意地址上的任意数据的;某些硬件平台只能在某些地址处取某些特定类型的数据,否则抛出硬件异常。
2. 性能原因:
数据结构(尤其是栈)应该尽可能地在自然边界上对齐。原因在于,为了访问未对齐的内存,处理器需要作两次内存访问;而对齐的内存访问仅需要一次访问。
总体来说:
结构体的内存对齐是拿空间来换取时间的做法。
其实默认对齐数是可以修改的。这里给大家介绍一个预处理指令。
#pragma pack(4) //修改默认对齐数4
注意:①一般情况下,我们修改的默认对齐数都是2的次方数,很少出现3,5这样的默认对齐数
②当我们修改默认对齐数并使用完之后,一定要取消设置默认的对齐数。
举个例子来为大家说明吧。
#pragma pack(1)//讲默认对齐数改为1
struct S5
{
char ch1;
int x;
char ch2;
};
#pragma pack() //取消设置默认对齐数
int main()
{
printf("%d\n", sizeof(struct S5));
return 0;
}
如上,当我们修改完默认对齐数之后,再利用规则就可以算出,结构体S5的大小为6。
结论:结构在对齐方式不合适的时候,我们可以自己更改默认对齐数。
最后,我们来学习一下结构体传参。有时候我们创建一个结构体变量之后,不会直接使用它,而是作为参数,来进行传参。
①直接以结构体变量名作为参数
②以结构体的地址作为参数
如下代码:
struct S
{
int data[1000];
int num;
};
//结构体传参
void print1(struct S s)
{
printf("%d\n", s.num);//点操作符解引用
}
//结构体地址传参
void print2(struct S* ps)
{
printf("%d\n", ps->num);//指针通过->解引用
}
int main()
{
struct S s = { {1,2,3,4}, 1000 };
print1(s); //传结构体
print2(&s); //传地址
return 0;
}
如上代码,我们通过两种方式传参都可以实现我们的效果。那么到底哪一种更合适呢,哪一种更好呢?其实是穿结构体指针更好一些,为什么呢?
当我们以结构体变量作为实参来传参的时候,实参传给形参的时候,我们知道,形参是实参的一块临时拷贝,它也需要准备一个很大的空间来存放拷贝过来的数据。函数传参的时候,系统会进行压栈的,这些系统消耗会比较大,浪费空间。
当我们以指针的形式传参的时候,系统只会开辟4/8个字节,消耗会比较小,省空间。
结论:
函数传参的时候,参数是需要压栈,会有时间和空间上的系统开销。
如果传递一个结构体对象的时候,结构体过大,参数压栈的的系统开销比较大,所以会导致性能的下降。也就是我们要传结构体指针。
好了,到这里,今天要讲的结构体相关的知识就讲完了,希望这篇文章对你起到一定的帮助,如果觉得小编写的还可以的,可以一键三连(点赞,关注,收藏)哦,你们的支持是对小编极大的鼓励,谢谢!!!