13. 结构体
一、什么是结构体
“结构体” 是一种构造类型,它是由若干 “成员” 组成的,其中的每一个成员可以是基本数据类型也可以是一个构造类型。既然构造体是一种新的类型,就需要先对其进行构造,这里称这种操作为声明一个结构体。
二、结构体的声明
我们需要使用 struct 关键字来声明结构体,其一般格式如下:
struct 结构体名
{
数据类型 成员1;
数据类型 成员2;
...
数据类型 成员n;
};
其中,struct 关键字表示声明结构体,其后的结构体名表示该结构体的类型名,大括号中的变量构成结构体的成员,也就是一般形式的成员列表,成员列表可以是任意一种 C 的数据类型,也可以是其它构造类型。右花括号后面的分号是声明所必须的,表示结构体声明的结束。
我们可以把结构体的声明放在所有函数的外部,也可以放在一个函数定义的内部。如果把结构体声明置于一个函数的外部,该结构体只限于在这个函数中使用;如果把结构体声明位置所有函数的外部,那么该声明之后的所有函数都可以使用这个结构体。
在声明结构体时,要注意括号最后面有一个分号“;”
结构体名一般每个单词的首字母大写;
三、结构体变量的定义
声明一个结构体表示的是创建一种新的类型名,需要用新的类型名在定义一个变量。不同结构体变量的成员是相互独立的,互补影响,一个结构体变量的更改,不会影响到另一个结构体。定义的方式有 3 种:
【1】、声明结构体类型,再定义变量
struct 结构体类型名 结构体变量;
【2】、在声明结构体时定义变量
struct 结构体名
{
数据类型 成员1;
数据类型 成员2;
...
数据类型 成员n;
}变量名1, 变量名2, ..., 变量名n;
定义的变量不是只能有一个,可以定义多个变量;
【3】、直接定义结构体类型变量
如果我们只需要确定个数的结构体变量,后面不需要再使用结构体数据类型定义变量,我们可以在定义时不给出结构体名,这种结构体称为 匿名结构体。
struct
{
数据类型 成员1;
数据类型 成员2;
...
数据类型 成员n;
}变量名1, 变量名2, ..., 变量名n;
四、结构体变量的初始化
我们可以在结构体类的声明,变量声明,赋初值同时进行。在初始化时,初始化的值放在大括号中,多个值使用逗号 “,” 分隔,并且每一个数据要与结构体的成员列表一一对应。
struct 结构体名
{
数据类型 成员1;
数据类型 成员2;
...
数据类型 成员n;
}变量名 = {值1, 值2, ..., 值n};
结构体类型与其它基本类型一样,也可以在定义结构体变量时指定初始值。
struct 结构体名 变量名 = {值1, 值2, ..., 值n};
#include <stdio.h>
struct Person
{
char name[20];
char gender[10];
int age;
}person={"Sakura","女",10};
int main(void)
{
printf("name: %s\n",person.name);
printf("gender: %s\n",person.gender);
printf("age: %d\n",person.age);
return 0;
}
C99 和 C11 为结构体提供了 指定初始化器(designated unitializer),它使用 点运算符 和 成员名 来表示特定的元素。它的语法格式如下:
struct 结构体名 变量名 = {
.成员1 = 值1,
.成员2 = 值2,
...,
.成员n = 值n
};
#include <stdio.h>
struct Person
{
char name[20];
char gender[10];
int age;
};
int main(void)
{
struct Person person = {
.name = "Sakura",
.age = 10
};
printf("name: %s\n",person.name);
printf("gender: %s\n",person.gender);
printf("age: %d\n",person.age);
return 0;
}
五、结构体变量的引用
对结构体变量进行赋值、存取或运算,实际上就是对结构体成员的操作。引用结构体成员的一个格式为:
结构体变量名.成员名
不能直接将一个结构体变量作为一个整体进行输入和输出;
六、包含结构体的结构体
结构体中的成员不仅可以是基本数据类型,也可以是结构体类型。
#include <stdio.h>
struct Date{
int year;
int month;
int day;
};
struct Person{
char name[20];
int age;
char gender[10];
struct Date birthday;
}person = {"Sakura",10,"女",{1987,4,1}};
int main(void)
{
printf("name: %s\n",person.name);
printf("age: %d\n",person.age);
printf("gerder: %s\n",person.gender);
printf("birthday:%d-%d-%d\n",person.birthday.year,person.birthday.month,person.birthday.day);
return 0;
}
七、结构体的自引用
在结构体中包含一个类型为该结构体本身的成员。
struct Node
{
int data;
struct Node * next;
};
八、结构体内存对齐
结构体的对齐规则:
- 第一个成员在结构体变量偏移量为 0 的地址。
- 其它成员变量要对齐到某个整数(对齐数)的整数倍的地址处。
- 对齐数 = 编译器默认的一个对齐数 与 该成员大小 的 较小值
- 结构体总大小为最大对齐数(每个成员变量都有一个对齐数)的整数倍。
- 如果嵌套了结构体的情况。嵌套的结构体对齐到自己的最大对齐数的整数倍处,结构体的整体大小就是所有最大对齐数(含嵌套结构体的对齐数)的整数倍。
#include <stdio.h>
#include <stddef.h>
// VS编译器默认对齐字节大小是8字节
// 其它编译器没有默认对齐数
// #pragma pack(1) // 修改默认对齐数
struct S
{
char c1; // 字节大小1字节
int i; // 字节大小4字节
char c2; // 字节大小1字节
}s1;
int main(void)
{
printf("%zu\n",sizeof(s1));
// offsetof宏可以返回一个成员在结构体中的偏移量
printf("%d\n", offsetof(struct S,c1));
printf("%d\n", offsetof(struct S,i));
printf("%d\n", offsetof(struct S,c2));
return 0;
}
#include <stdio.h>
struct S1
{
double d; // 字节大小8字节
char c; // 字节大小1字节
int i; // 字节大小4字节
};
struct S2
{
char c; // 字节大小1字节
struct S1 s1;
double d; // 字节大小1字节
}s2;
int main(void)
{
printf("%zu\n",sizeof(s2));
return 0;
}
为什么要存在结构体内存对齐呢?大部分的参考资料是如下解释的:
①、平台原因
不是所有的硬件平台都能访问任意地址上的任意数据的;某些硬件平台只能在某些地址处取某些特定类型的数据,否则抛出硬件异常;
②、性能原因
数据结构(尤其是栈)应该尽可能在自然边界对齐。原因在于,为了访问未对其的内存,处理器需要做两次内存访问;而对齐的内存仅需要一次访问。
总体上来说,结构体的内存对齐是拿空间获取时间的做法;
我们在设计结构体时,让占用空间小的成员尽量集中在一起,这样既可以满足内存对齐,又可以节省空间;