目录
1.结构体简介
结构是⼀些值的集合,这些值称为成员变量。结构的每个成员可以是不同类型的变量。
在C语言中,我们可以定义结构体变量来方便的创建变量的集合。
2. 结构体的声明
struct tag // tag为结构体标签,struct tag为结构体的类型名
{
member-list;
}variable-list;
比如可以创建一个结构体用以存储学生信息:
struct Stu
{
char name[20];//名字
int age;//年龄
char sex[5];//性别
}
2.1结构体的创建以及初始化
例如:
#include <stdio.h>
struct Stu
{
char name[20];//名字
int age;//年龄
char sex[5];//性别
char id[20];//学号
};
int main()
{
//按照结构体成员的顺序初始化
struct Stu s = { "张三", 20, "男", "20230818001" };
return 0;
}
可以看到,我们可以按照结构体定义的顺序,通过逗号分隔内容来对结构体变量进行初始化。
同时,类似于数组的初始化方式,我们也可以使用不完全初始化:
struct Stu s = { "张三", 20, "男"};
struct Stu s = {0}; //两种初始化方法都是不完全初始化,未赋值的部分会自动赋值为0
2.1.1结构体的特殊声明
//匿名结构体类型
struct
{
int a;
char b;
float c;
}x;
struct
{
int a;
char b;
float c;
}a[20], *p;
然而,匿名结构体类型存在的问题是,即使内部成员完全相同,类似于下面的代码是非法的:
p = &x;
这是因为编译器在进行类型检查时会将两种类型视为不同类型,所以说匿名结构体类型的声明是有风险的,如果使用了就只能使用一次。
3.结构体的自引用(无法嵌套)
结构体自身无法嵌套,也就是说结构体的内部成员不可以是他本身,但是如下的引用在结构体中却是可行的。
struct Node
{
int data;
struct Node* next; //该成员是结构体指针类型的变量,是结构体内合法的自身引用
};
虽然在结构体中包含自身的结构体指针是可以的,但是如下的做法却是错误的:
typedef struct
{
int data;
Node* next; //在重命名执行前就使用了该变量名
}Node;
这是语序的问题,typedef的重命名是在结构体的声明后才完成的,但是结构体的声明中却包含了重命名的变量名,所以会有错误。
4.结构体传参
结构体虽然和数组一样可能占据相当大的空间,但是结构体在传参的时候形参是实参的复制,所以在函数中传入结构体时应该传入指针以节省消耗。
5.结构体大小的计算(结构体内存对齐)
struct S1
{
char c1;
int i;
char c2;
};
//printf("%d\n", sizeof(struct S1));
//输出结果:12
struct S2
{
char c1;
char c2;
int i;
};
//printf("%d\n", sizeof(struct S2));
//输出结果:8
可以看到两个结构体所含有的成员类型相同,只是排列顺序不同,可是其大小却有差异,
这是因为结构体成员在内存中的排列有以下这些规则:
1. 结构体的第⼀个成员对齐到和结构体变量起始位置偏移量为0的地址处
2. 其他成员变量要对齐到某个数字(对齐数)的整数倍的地址处。 对齐数 = 编译器默认的一个对齐数 与 该成员变量大小的较小值。
- VS 中默认的值为 8 - Linux中 gcc 没有默认对齐数,对齐数就是成员自身的大小
3. 结构体总大小为最大对齐数(结构体中每个成员变量都有⼀个对齐数,所有对齐数中最大的)的整数倍。
4. 如果嵌套了结构体的情况,嵌套的结构体成员对齐到自己的成员中最大对齐数的整数倍处,结构体的整体大小就是所有最大对齐数(含嵌套结构体中成员的对齐数)的整数倍。
注意:对于数组来说,对齐数是其每个元素的对齐数,例如int[10]的对齐数为4,
char[10]对齐数为1;
S1和S2在内存中的排列:
可见偏移量就是从结构体分配的内存的最低地址为起始开始计算的量,是地址的偏移量,
其成员按照结构体中的声明顺序,在结构体开辟的内存中从低地址向高地址排列,然后按照各自的偏移量进行排列,比如int类型在vs环境下对齐数是4,需要排列在偏移量为4的倍数的位置。
最后还要根据结构体中最大对齐数的大小来计算结构体的总大小,所以即使成员已经排列完成,之后可能依然会有内存的浪费(如例中S1)。
理解原理之后就要尽量“插空”排列结构体成员,使其所占空间最小。
5.1为何存在内存对齐
移植原因:某些硬件平台只能在某些地址处取某些特定 类型的数据,否则抛出硬件异常。内存对齐使得数据在内存中存储的地址具有特殊性,可以方便取用。
性能原因:数据结构(尤其是栈)应该尽可能地在自然边界上对齐。原因在于,为了访问未对齐的内存,处理器需要作两次内存访问;而对齐的内存访问仅需要⼀次访问。假设⼀个处理器总是从内存中取8个字节,则地址必须是8的倍数。如果我们能保证将所有的double类型的数据的地址都对齐成8的倍数,那么就可以用⼀个内存操作来读或者写值了。否则,我们可能需要执行两次内存访问,因为对象可能被分放在两个8字节内存块中。