前言:我们知道,C语言本身有许多变量类型,例如char,short,int,long,long long,float,double,long double等,这些C语言本身支持的现成的类型称为内置类型,然而仅仅使用这些内置变量无法一些复杂对象进行描述,就一本书而言,如何去描述它呢?它有名字,编号,作者,日期等等....这时我们发现仅仅用内置类型是不够的,事实上,C语言中不仅仅有内容变量,也允许有自定义类型,包括:结构体—struct,枚举—enum,联合体—union。今天我们的主角便是自定义类型之一:结构体变量。
目录:
一.结构体定义
1.1结构体的声明
1.2结构体变量的创建和初始化
1.3结构体的特殊声明
1.4结构体的自引用
二.结构体内存对齐
2.1对齐规则
2.2有关内存对齐的例题介绍
一.结构体定义
何为结构体?事实上,结构就是是⼀些值的集合,这些值称为成员变量。结构的每个成员可以是不同类型的变量。
1.1结构体的声明
struct tag
{
member-list;//成员变量
}variable-list;//切记分号不能丢
struct tag称为结构体标签,相当于结构体变量类型,类似与int,char等
而variable-list是你给这个结构体指定的变量名称,类似于int a=0;中的a,它不一定要出现在结构体的声明中,也可以在使用它的时候再创建名称。
例如:试想如何描述一个学生,他有名字,年龄,性别,学号等。
struct Stu
{
char name[20];//姓名
int age;//年龄
char gender[5];//性别
int id[10];//学号
};
1.2结构体变量的创建和初始化
以上文学生为例,我们已经声明了这样一个描述他具体信息的结构体变量,那如何给它初始化赋值呢?
这里就需要引入一个新操作符,"."操作符,称为结构体成员变量引用操作符,通过使用.可以找到结构体中的成员变量.
. 结构体.成员变量名
法一:按照结构体成员的顺序初始化
#include <stdio.h>
struct Stu
{
char name[20];//名字
int age;//年龄
char sex[5];//性别
char id[20];//学号
};
int main()
{
//按照结构体成员的顺序初始化
struct Stu s = { "张三", 20, "男", "20230818001" };
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;
}
法二:按照指定顺序初始化
#include <stdio.h>
struct Stu
{
char name[20];//名字
int age;//年龄
char sex[5];//性别
char id[20];//学号
};
int main()
{
//按照指定顺序初始化
struct Stu s2 = { .age = 18, .name = "lisi", .id = "20230818002", .sex =
"⼥" };//乱序
printf("name: %s\n", s2.name);
printf("age : %d\n", s2.age);
printf("sex : %s\n", s2.sex);
printf("id : %s\n", s2.id);
return 0;
}
最终我们都如愿以偿地得到了想要初始化的结果:
1.3结构体的特殊声明
在声明结构体的时候,有一种特殊的声明方法,就是不完全声明。
例如:
//匿名结构体类型
struct
{
int a;
char b;
float c;
}x;
struct
{
int a;
char b;
float c
}a[20],*p;
上面两个结构体在声明的时候都省略掉了结构体标签(tag),那么问题来了:
在上⾯代码的基础上,下⾯的代码合法吗?
p = &x;
警告: 编译器会把上⾯的两个声明当成完全不同的两个类型,所以是⾮法的。 匿名的结构体类型,如果没有对结构体类型重命名的话,基本上只能使⽤⼀次。也就是说,匿名结构体可以使用,但它是一次性的,不可二次操作。
1.4结构体的自引用
在结构体的定义中我们了解到,结构体的成员变量可以是任意类型的,那么在一个结构体当中是否可以包含一个类型为该结构体本身的成员呢?
例如,定义一个链表(一种数据结构)的节点:
struct Node
{
int data;
struct Node next;
};
上述代码是否正确?如果正确,那么试想sizeof(struct Node)是多少?仔细分析就会发现这个代码是不行的,若是一个结构体再包含一个同类型的结构体变量,这样结构体大小就会无穷大,像是陷入了一个死循环,是不合理的。那是不是结构体无法自引用呢?也不是,需要正确的方式。
正确的自引用方式:
struct Node
{
int data;
struct Node* next;
};
与上一代码不同,正确的自引用方式应是借助指针。
在结构体⾃引⽤使⽤的过程中,夹杂了 typedef 对匿名结构体类型重命名,也容易引⼊问题,看看 下⾯的代码,可⾏吗?
typedef struct
{
int data;
Node* next;
}Node;
答案是不⾏的,因为Node是对前⾯的匿名结构体类型的重命名产⽣的,但是在匿名结构体内部提前使 ⽤Node类型来创建成员变量,这是不⾏的。(简要说就是人家没创建好你就提前用了,当然不可以了)那如何解决呢?一句话:定义结构体时不要使用匿名结构体!
typedef struct Node
{
int data;
struct Node* next;
}Node;
二.结构体内存对齐
在上文我们提及了一次sizeof(struct Node),你是否思考过一个结构体的大小是多少呢?简单来想,一个结构体有多大难道不是各个成员变量所占内存大小的总和吗,事实并非如此,例如:
按照我们的设想,这个结构体大小应该是1+4+1=6个字节,然而事实上它占了12个字节,这是为什么呢?这就要引出一个热门考点:结构体的内存对齐规则。
2.1对齐规则
首先让我们了解一下结构体对齐规则的理论:
1.结构体的第⼀个成员对⻬到和结构体变量起始位置偏移量为0的地址处;
2.其他成员变量要对⻬到某个数字(对⻬数)的整数倍的地址处。
对齐数=编译器默认的一个对齐数与该成员变量大小的较小值。
-VS中的默认值为8。
-Linux中的gcc没有默认对齐数,对齐数就是成员自身大小。
3.结构体总大小为最大对齐数(结构体中的每个成员变量都有一个对齐数,所有对齐数中最大的)的整数倍数。
4.如果是嵌套了结构体的情况,嵌套的结构体成员对齐到自己成员中最大对齐数的整数倍处,结构体的整体大小就是所有最大对齐数(含嵌套结构体中成员的对齐数的整数倍)。
2.2有关内存对齐的例题介绍
理论成立,下面我们具体分析一下:
再拿出刚才的例子:
struct Stu
{// 成员变量大小 编译器默认对齐数 最终对齐数(二者较小值)
char a;// 1 8 1
int b;// 4 8 4
char c;// 1 8 1
};
在内存中观察可知,第一个成员先对齐到初始偏移量为0的地址处,剩下的成员要对齐到各自对齐数的整数倍地址处,例如int b的对齐数为4,所以它要跳过三个字节从4(是4的整数倍)处开始存储,同理对于char c,它的对齐数是1,可以紧挨着int b存储,因为任何整数都是1的整数倍,最终结构体还要对齐到最大对齐数(即所有成员变量的对齐数中最大的,即4),所以最终结构体还要跳过3个字节指向12(4的整数倍处),这样才算开辟了一个完整的结构体空间,实现了结构体的内存对齐。 这样我们便不难理解上述例子结构体大小为何是12而不是6了。
练习一下吧:
#include<stdio.h>
struct S3
{
double d;//8 8 8
char c;//1 8 1
int i;//4 8 4
};
//结构体嵌套问题
struct S4
{
char c1;//1 8 1
struct S3 s3;//对齐到自身成员变量的最大对齐数处
double d;//8 8 8
};
int main()
{
printf("%zd\n", sizeof(struct S3));//16
printf("%zd\n", sizeof(struct S4));//32
return 0;
}
如何计算的和上述例子是一样滴,望同学们仔细观察!