结构体
自定义类型—结构体
-
结构体是用于将多个数据类型或相同数据类型的元素组合在一起形成一个数据类型集合,变成一个有意义的整体。在现实的工作项目中通过对结构体内部成员变量的操作将大量的数据存储在内存中,以实现对数据的存储和操作等。
-
结构体是一些值的集合,这些值我们称为成员变量。结构体的每个成员可以是不同类型的变量,如标量,数组,指针,甚至可以是其他结构体类型;
-
结构体的大小并不是其成员大小的简单相加。由于内存对齐的原因,编译器可能会在结构体成员之间插入填充字节,以确保每个成员的首地址都是特定对齐系数的整数倍。这样做可以提高数据访问的效率,但也会增加结构体占用的总内存空间。
1.结构体类型的声明
- 定义结构体的一般模式:
struct [tag]
{
member-list
} [declarators];
-
struct
: 声明一个结构体类型。 -
[tag]
: 结构体的标签(tag),也就是结构体的名字。标签是可选的,如果省略了标签,那么结构体就称为无名结构体或者匿名结构体(anonymous struct)。在有标签的情况下,后续就能使用这个标签来定义该类型的变量。如果没有标签名,那么结构体就只能使用一次 -
{ member-list }
: 这是结构体的主体部分,用大括号{}
包围起来。member-list
列出了结构体的所有成员,每个成员都有自己的类型和名字,成员之间用分号 ; 分隔开。 -
[declarators]
: 这是结构体的声明符列表,用于定义结构体的变量。这也是可选的,如果省略了这部分,那么只是声明了结构体类型,而没有定义任何该类型的变量。
struct student // [tag] 就是 Student
{
char name[20];//member-list中的第一个成员
int age;//member-list中的第二个成员
char sex;//member-list中的第三个成员
char id[15];//member-list中的第四个成员
}student1,student2;// [declarators] 定义了两个 Student 类型的变量 student1 和 student2
1.1结构体的特殊声明
我们还可以只声明结构体类型而不定义变量,那么可以省略 [declarators]
部分:
struct student // [tag] 就是 Student
{
char name[20];
int age;
char sex;
char id[15];
};//分号不能掉;
在这种情况下,后续可以单独使用 struct Student
来定义该类型的变量。
如果结构体只使用一次,并且不想为其命名,可以省略 [tag]
,从而得到匿名结构体:
struct
{
char name[20];
int age;
char sex;
char id[15];
}student1,student2;//分号不能掉;
由于结构体没有结构体名,因此不能重复使用该结构体,只能使用一次这两个变量 student1
和 student2
。
注意:
- 编译器会把上面的两个声明当成完全不同的两个类型;
- 匿名结构体类型,如果没有对结构体类型重命名的话,基本上只能使用一次;
2.结构体变量的创建和初始化
#include <stdio.h>
struct Stu//定义一个名为Stu的结构体
{
char name[20];//name(用于存储学生的名字,最多20个字符)
int age;//age(用于存储学生的年龄)
char sex[20];//sex(用于存储学生的性别,最多20个字符)
char id[20];//id(用于存储学生的ID/学号,最多20个字符)
};
int main()
{
//按照结构体成员的顺序初始化
struct Stu u = { "lyz",30,"男","20240428" };
printf("name: %s\n",u.name );
printf("age : %d\n",u.age );
printf("sex : %s\n",u.sex );
printf("id : %s\n",u.id );
//按照指定顺序初始化
//C99引入的指定初始化器语法,允许按照任意顺序初始化结构体的成员。
struct Stu s = { .age = 18,.name = "fzy",.id = "20230504",.sex = "男"};
printf("name: %s\n", s.name);
printf("age : %d\n", s.age);
printf("sex : %s\n", s.sex);
printf("id : %s\n", s.id);
return 0;
}
2.1 结构体的自引用
结构体的自引用指的是结构体类型中包含一个指向自身类型的指针。
自引用的方式通常用于构建如链表、树等高级数据结构。在C语言中,结构体的自引用通常用于创建链表节点,其中每个节点都包含一个指向下一个节点的指针,从而形成一个链表。例如:
struct Node
{
int data;
struct Node* next;
};
需要注意的是,我们 不能 写成这样:
struct Node
{
int data;
struct Node next;//Node结构体包含一个完整的Node结构体
};
错误在于struct Node next;
这一行。这里尝试将next
成员定义为struct Node
类型,而不是指向struct Node
类型的指针。这将导致无限递归定义,因为每个Node
结构体都包含一个完整的Node
结构体,而这个Node
结构体又包含一个完整的Node
结构体,以此类推,无穷无尽。这样结构体变量的大小sizeof(struct Node)
就会无穷大。
再看一段问题代码:
typedef struct Node
{
int data;
Node* next;
}Node;
这里有一个潜在的错误:结构体名字和结构体类型名使用了相同的名称Node
。在C语言中,当typedef
声明中直接使用结构体名字时,编译器无法正确识别Node*
中的Node是指向结构体的指针
还是结构体的标签
。
所以定义结构体不要使用匿名结构体,我们可以这样修改:
typedef struct Node
{
int data;
struct Node* next;
}Node;
3.结构体内存对齐
结构体的大小与结构体的的对齐规则息息相关:
3.1 对齐规则
规则:
- 结构体的第一个数据成员对齐 到 结构体变量起始位置偏移量为0的地址处(即即结构体的起始地址)
- 其他数据成员变量对齐 到 对齐数的整数倍地址处
- 对齐数 = 编译器默认的一个对齐数 与 该成员变量大小的较小值;
-
VS中默认的对齐数是8;
-
Linux中gcc没有默认对齐数,对齐数就是成员自身的大小
-
- 对齐数 = 编译器默认的一个对齐数 与 该成员变量大小的较小值;
- 结构体大小为最大对齐数(结构体中每个成员变量都有一个对齐数,所有对齐数中最大的)的整数倍;
- 对于嵌套了结构体的情况,嵌套的结构体成员对齐到自己的成员中最大对齐数的整数倍处,结构体的整体大小就是所有最大对齐数(含嵌套结构体中成员的对齐数)的整数倍;
3.2 结构体大小与对齐规则
分析:
以这段代码为例:
struct S1
{
char c1;//一个字节
int i;//4个字节
char c2;//1个字节
};
printf("%d\n",sizeof(struct S1));
- 结构体的第一个数据成员对齐 到 结构体变量起始位置偏移量为0的地址处(即结构体的起始地址),如图:
- 其他数据成员变量对齐 到 对齐数的整数倍地址处
- 首先我们要先计算出对齐数(以VS中的默认对齐数为例):
- 结构体中的第二个成员变量
int i
的大小为4
个字节,VS的默认对齐数是8
,取它们两个中的较小值即4
,即对齐数为4,所以第二个数据i
存放位置的是4的倍数的地址处,然后向下取4个字节,如图:
- 结构体中的第二个成员变量
- 结构体中的第三个成员变量
char c2
的大小为1
个字节,VS的默认对齐数是8
,取它们两个中的较小值即1
,即对齐数为1,所以第三个数据c2
存放位置的是1的倍数的地址处,然后取1个字节,如图:
- 首先我们要先计算出对齐数(以VS中的默认对齐数为例):
- 结构体大小为最大对齐数(结构体中每个成员变量都有一个对齐数,所有对齐数中最大的)的整数倍;
char c1
的对齐数是1
,int i
的对齐数为4
,char c2
的对齐数是1
,其中最大的对齐数是4
,所以结构体的大小会以填充字节的方式,补全结构体大小,以达到4的倍数,我们已经从0 ~ 8占用了9个字节的大小,9不是4的倍数,所以编译器会在结构体的末尾添加填充字节,确保整个结构体的大小满足对齐要求,故结构体的大小会被补到序号为11
的位置,即12个字节的大小,如图:
- 所以例子中
struct
结构体的大小为12
字节。
对于嵌套结构体
的大小:
struct S2
{
double d;
char c;
int i;
};
printf("%d\n",sizeof(struct S2));
struct S3
{
char c1;
struct S2 s2;
double d;
};
printf("%d\n",sizeof(struct S4));
-
计算嵌套结构体之前我们需要先计算嵌套的结构体
S2
的大小,所以我们来看S2
的大小计算过程:- 第一个变量是
double d
,大小是8个字节,根据对齐规则,第一个变量放在存储结构体的起始位置,即偏移量为0的地方,如图:
- 第一个变量是
-
然后我们看第二个变量
char c
,大小为1
个字节,与编译器默认对齐数8
相比,取较小的值,即对齐数为1
,所以存放c
的地方的起始地址序号必须是1的倍数,如图:
-
接着看int i 的大小为4个字节,与编译器默认对齐数
8
相比,取较小的值,即对齐数为4
,所以存放i
的地方的起始地址序号必须是4的倍数,如图:
-
最后取结构体中最大的对齐数,即double的8作为最大对齐数,结构体内存必须是最大对齐数的整数倍,所以结构体的大小为16个字节。(如上图所示,从0到15,共16个空间,是8的倍数);
接下来我们再来计算结构体S3
的大小:
-
首先来看第一个变量
char c1
,大小为1个字节,存放在结构体的起始地址处,如图:
-
再来看结构体
S3
中的结构体S2
的对齐数,根据刚刚的计算我们知道S2
中最大的对齐数是8,那么存放位置的起始地址的序号必须是8的倍数,并且S2的代销为16个字节,所以向下16个空间,如图:
-
接着来看第三个变量
double d
的大小是8个字节,与编译器默认对齐数8相比,最终得到的对齐数还是8,所以从8的倍数的位置向后储存,如图:
-
最后判断结构体的最终大小是否满足所有对齐数的整数倍,
char c1
的对齐数是1
,结构体S2
的最大对齐数是8
,double d
的最大对齐数是8
,三个对齐数比较,其中最大对齐数是8
,再看结构体S3
的大小为32
(从0 ~ 31,共32个空间),满足最大对齐数8的倍数的要求,所以嵌套结构体S3
的大小为32
;
补充:结构体满足对齐规则的同时,存在浪费空间的情况,可以看到我们计算结构体大小时,中间存在空的内存没有储存数据(图中空白的地方和最后为了满足倍数要求而额外多占用内存的地方(中间空白和图中褐色处));
我们无法改变对齐规则,但是为了尽量解决空间浪费的问题,所以我们在创建结构体时,尽量让占用空间小的成员集中到一起(不一定从开头开始放,集中放在一起即可);
4.修改默认对齐数
#pragma pack
:这个预指令可以控制结构体中成员的对齐方式,即修改默认对齐数。
#pragma pack
可以指定一个对齐数(“打包值”或“对齐边界”),让编译器按照这个对齐数来进行结构体成员的内存对齐。
例如:
#include <stdio.h>
// 设置对齐数为1字节
#pragma pack(1)
struct Stu {
char c;
int i;
};
#pragma pack() // 取消之前设置的对齐数,恢复默认对齐数设置
int main() {
printf("Size of MyStruct: %zu bytes\n", sizeof(struct MyStruct));
return 0;
}
5.结构体传参
结构体传参时,我们是直接传结构体过去,还是将结构体的地址过去呢?
#include <stdio.h>
struct S
{
int data[1000];
int num;
};
struct S s{{1,3,1,4},520};
//结构体传参
void print1(struct S s)
{
printf("%d\n",s.num);
}
//结构体地址传参
void print2(const struct S* ps)
{
printf("%d\n",ps->num);
}
int main()
{
print1(s);//传结构体
print2(&s);//传地址
return 0;
};
在这段代码中我们进行结构体传参时,首选print2
函数的传参方式
原因:
- 函数传参时,参数需要压栈,会有时间和空间上的系统开销;
- 如果传递一个结构体对象时,结构体过大,参数压栈的系统开销比较大,所以可能会导致性能的下降;
结论:结构体传参时,要传结构体的地址。