目录
1.什么是结构体类型
2.结构体变量的创建
3.结构体变量的初始化
4.结构体的特殊声明
5.typedef重定义结构体变量(两种方式)
6.结构体自引用
7.创建结构体指针变量
8.结构体内容的访问
8.1直接访问:
8.2 结构体指针访问
9.结构体内存存储
9.1 结构体内存存储4条规则
例题1&&讲解:
结构体内容的储存方式:
结构体字节的大小:
例题2&&讲解:
结构体内容的储存方式:
结构体字节的大小:
例题3&&讲解:
结构体内容的储存方式:
结构体字节的大小:
例题4&&讲解(结构体嵌套问题 )
结构体内容的储存方式:
结构体字节的大小:
10.为什么存在内存对齐?
11.如何节省结构体内存大小
12.修改默认对齐数
13.scand初始化结构体变量
错误写法如下编辑:
14.结构体传参
编辑
小心!VS2022不可直接接触,否则!没这个必要,方源面色淡然一把抓住!顷刻炼化!
1.什么是结构体类型
什么是结构体类型,这里拿一个常见的格式举例:
struct stu //struct stu 为结构体类型 { int a; //结构体内容 }s1; //s1 为结构体变量
这是一个很常见的结构体格式
struct stu 为结构体类型
s1 为结构体的变量
int a 为结构体变量的内容
2.结构体变量的创建
结构体变量的创建有两种方法,一种是随着结构体的创建而创建
另一种则是单独创建,示例如下:
方法一:
struct stu1{ int a; }s1; //创建结构体变量s1
方法二(含注意事项):
struct stu{ int a; }; //注意这里的 ; 一定要写,不写就是错误的结构体 struct stu s1; //创建结构体变量s1
这里采用的是 用结构体类型创建结构体变量
不理解的可以类比int a,类型加变量名
3.结构体变量的初始化
结构体变量的初始化可以分为三种情况:
情况1&&情况3:
变量随着结构体而定义
定义结构体变量需要采用,变量名.结构体内容 进行初始化结构体内容情况1: 变量随着结构体而定义 定义结构体变量需要采用,变量名.结构体内容 进行初始化结构体内容 示例如下 (情况一): struct stu //结构体类型为struct stu { int a; char arr[20]; int d; }s1, s2; //结构体变量为s1,s2 //结构体初始化: s1.a = 20;//相等于直接访问结构体内容,并直接赋值 s1.arr[0] = 'w'; //对单个字符型数组存储'w' printf("%c \n",s1.arr[0]);//打印字符w printf("%d ",s1.a);//打印20 ------------------------------------------------------ ------------------------------------------------------ (情况三)直接将变量初始化 struct stu //结构体类型为struct stu { int a; char arr[20]; int d; }s1 = {20,"w",987}; //从数组起始地址开始存储"w" //直接将变量初始化
情况2:
变量不随着结构体而定义
定义结构体变量采用, 结构体类型+变量名 的形式进行初始化结构体内容情况2: 变量不随着结构体而定义 定义结构体变量采用, 结构体类型+变量名 的形式进行初始化结构体内容 示例如下:情况2 struct stu //结构体类型为struct stu { int a; char arr[20]; int d; }; struct stu s1 = { 20,"w",987 }; //按照顺序初始化结构体内容 struct stu s2 = { .a = 90,.d = 98799,.arr[0] = 'd' }; //指定顺序初始化,采用 .结构体内容的形式 初始化 printf("%d %c %d\n", s1.a, s1.arr[0], s1.d); //打印20 w 987 printf("%d %c %d\n", s2.a, s2.arr[0], s2.d); //打印90 d 98799
4.结构体的特殊声明
如果结构体声明不完全,创建的结构体只能使用一次,示例如下:
struct { //结构体未完全声明 int a; int b; }s1; s1.a = 20; s1.b = 30; //未声明的结构体属于匿名结构体类型,只能使用一次 s2.a = 40; //使用完变量s1后再想创建新的变量s2 //但是因为是匿名结构体类型,所以系统会报错,不能再次创建和使用新的变量
5.typedef重定义结构体变量(两种方式)
第一种方式:
typedef struct stu node;//将结构体类型 struct stu 重定义为 node struct stu { int a; }; node s1 = { 20 }; //这里的node就相当于struct stu printf("%d ", s1.a); //这里是结构体的直接访问,变量名.结构体内容,可以直接访问到结构体内容 //创建结构体指针 node * p1 = &s1; //后面讲
第二种方式:
typedef struct stu { int a; }node; //这里不是创建结构体变量node,而是将struct stu重定义为node node s1 = { 20 }; //这里的node就相当于 struct stu printf("%d ", s1.a); //直接访问结构体变量s1中的a,打印20
6.结构体自引用
结构体自引用是一件很可怕的事,结构体最怕的就是重命名,一旦重命名就会出现错误
比如说结构体的变量与结构体的内容重名就会出现错误
那么什么是结构体的自引用呢?结构体的自引用就是一个结构体嵌套自己的结构体
示例如下:
struct stu { int a; struct stu s1; //死循环,变量s1里还有结构体,又会创建新的变量s1,空间无限大 };
使用自己的结构体嵌套自己,就会出现无数多个自己的结构体,结构体的内存就会无限大,是不合理的
7.创建结构体指针变量
对于结构体来说,结构体在未创建变量时不占用内存
所以结构体的变量名会表示整个结构体,因此&结构体变量名,就相当于取出了整个结构体变量的内存
那么如何创建结构体指针变量呢?示例如下:
创建结构体指针变量第一种写法:
struct stu { int a;//创建结构体指针变量p1,指针变量p1的类型为struct stu * }s1; //创建结构体指针变量 p1,存放结构体变量s1的地址 struct stu* p1 = &s1; //结构体指针变量p1的类型是struct stu*
相信有的同志会写出这样的代码:
struct stu { int a;//创建结构体指针变量p1,指针变量p1的类型为struct stu * }s1; struct stu * p1 = &s1; printf("%p\n", &s1);//打印012FFA30 printf("%p\n", p1); //打印012FFA30
其实这样写也是对的,因为在结构体大括号外的 ; 写完之后,再写句子就是单独的表达式了,所以其实和第一种写法是完全相同的,只是代码放的位置不一样
创建结构体指针变量第二种写法:
struct stu { int a;//创建结构体指针变量p1,指针变量p1的类型为struct stu * }*p;
8.结构体内容的访问
结构体内容的访问可分为两种,一种为直接访问,另一种为通过结构体指针访问
8.1直接访问:
struct stu //结构体类型为struct stu { int a; char arr[20]; int d; }s1 = { 20,"w",98}; printf("%d %c %d", s1.a, s1.arr[0], s1.d); //变量名.结构体成员 //打印20 w 98
8.2 结构体指针访问
#define _CRT_SECURE_NO_WARNINGS #include<stdio.h> int main() { struct stu //结构体类型为struct stu { int a; char arr[20]; int d; }s1 = { 20,{"w"},98 }; struct stu* p1 = &s1; //将结构体变量s1的地址 存放进p1中 printf("%d %d %c",p1->a,p1->d,p1->arr[0]); //结构体指针->结构体成员 //打印20 98 w
9.结构体内存存储
9.1 结构体内存存储4条规则
结构体内存储存方式有以下4条规则,也叫做结构体内存对齐原则
1.结构体的第一个成员对齐到和结构体变量起始位置偏移量为0的地址处
2.其他成员变量要对齐到某个数字(对齐数)的整数倍的地址处
对齐数 = 编译器默认的第一个对齐数与该成员变量大小的较小值
vs中默认的值为8
Linux中gcc没有默认对齐数,对齐数就是成员自身的大小
3.结构体的总大小为最大对齐数(结构体中每个成员都有一个对齐数,所有对齐数中最大的)的整数倍
4.如果嵌套了结构体,嵌套的结构体成员对齐到自己的成员中最大对齐数的整数倍,结构体的整数大小就是所有最大对齐数(含嵌套结构体中成员的对齐数的整数倍)
多说无益,我们拿 代码例题+配图 来讲解
例题1&&讲解:
//例题1:
struct S1
{
char c1;
int i;
char c2;
};
printf("%zd ", sizeof(struct S1));
//打印12
首先结构体的内存存储默认从0开始存储,我们以例题讲解
结构体内容的储存方式:
第一个结构体内容为char c1,结构体成员 c1 占用1个字节大小,结构体默认从0位置开始存储,那就从0开始存储1个字节大小的内存
第二个结构体内容为int i,结构体内容 i 占用4个字节,但因为结构体内存对齐原则,占用4个字节的int i,只能从 4 的整数倍数处(也叫结构体成员 i 的对齐数的整数倍)开始存储内存,所以我们直接跳到 4 的位置,从4的位置开始向下存储4个字节大小,4 5 6 7都是 i 的内存存储处
第三个结构体内容为char c2,结构体成员 c2 占用一个字节,因为结构体内存对齐原则,占用1个字节大小的char c2只能从1 的整数倍数处(对齐数的整数倍)开始存储内存,所以接着从 7 的位置向下存储1个字节大小的内存,即:将8的位置设为char c2 的内存存储处
结构体字节的大小:
在我们清楚完毕结构体内存的存储位置后,因为结构体的总大小为最大对齐数(结构体中每个成员都有一个对齐数,所有对齐数中最大的)的整数倍,在此例题中结构体成员的最大对齐数为4,也就是int i 中的 i 的对齐数为4,
因为结构体内存存储占用了9个字节的大小,所以我们取4的整数倍数(最大对齐数)12,最终大小被填充到12个字节
例题2&&讲解:
//例题2:
struct S2
{
char c1;
char c2;
int i;
};
printf("%zd", sizeof(struct S2));
//打印8
结构体内容的储存方式:
第一个结构体内容为char c1,结构体成员c1占用1个字节大小,结构体默认从0位置开始存储,那就从0开始存储1个字节大小的内存
第二个结构体的内容为char c2,结构体成员c2占用1个字节,但因为结构体内存对齐原则,占用1个字节的char c2,只能从 1 的整数倍数处(也叫c2 的对齐数的整数倍)开始存储内存,所以向下1个字节的位置为c2的内存存储地址
第三个结构体内容为int i,结构体成员 i 占用4个字节,因为结构体内存对齐原则,占用4个字节大小的int i 只能从4 的整数倍数处(对齐数的整数倍)开始存储内存,所以我们从 4 的整数倍处开始存储4个字节,4 5 6 7都为结构体变量内容 i 的存储地址
结构体字节的大小:
在我们清楚完毕结构体内存的存储位置后,因为结构体的总大小为最大对齐数(结构体中每个成员都有一个对齐数,所有对齐数中最大的)的整数倍,在此例题中结构体成员的最大对齐数为4,也就是int i 中的 i 的对齐数为4,
因为结构体内存存储占用了8个字节的大小,所以我们取4的整数倍数(最大对齐数)8,最终大小被填充到8个字节
例题3&&讲解:
struct S3
{
double d;
char c;
int i;
};
printf("%zd", sizeof(struct S3));
//打印16
结构体内容的储存方式:
第一个结构体内容为double d,结构体成员 d 占用8个字节大小,结构体默认从0位置开始存储,那就从0开始存储8个字节大小的内存
第二个结构体的内容为char c,结构体成员 c 占用1个字节,但因为结构体内存对齐原则,占用1个字节的char c,只能从 1 的整数倍数处(也叫c 的对齐数的整数倍)开始存储内存,所以向下1个字节的位置为 c 的内存存储地址
第三个结构体内容为int i,结构体成员 i 占用4个字节,因为结构体内存对齐原则,占用4个字节大小的int i 只能从4 的整数倍数处(对齐数的整数倍)开始存储内存,所以我们从 4 的整数倍处开始存储4个字节,因为8的位置已经被存储了,所以我们从下一个整数倍数--也就是12的位置开始存储,12 13 14 15都为结构体变量内容 i 的存储地址
结构体字节的大小:
在我们清楚完毕结构体内存的存储位置后,因为结构体的总大小为最大对齐数(结构体中每个成员都有一个对齐数,所有对齐数中最大的)的整数倍,在此例题中结构体成员的最大对齐数为8,也就是double d 中的 d 的对齐数为4,
因为结构体内存存储占用了16个字节的大小,所以我们取最大对齐数 8 的整数倍数(最大对齐数)16,最终大小被填充到16个字节
例题4&&讲解(结构体嵌套问题 )
struct S3 {
double d;
char c;
int i;
};
//S3字节为16
struct S4
{
char c1;
struct S3 s3;
double d;
};
printf("%zd ", sizeof(struct S4));
//打印32
结构体内容的储存方式:
第一个结构体内容为char c1,结构体成员 c1 占用1个字节大小,结构体默认从0位置开始存储,那就从0开始存储1个字节大小的内存
第二个结构体的内容为struct S3 s3,结构体成员 s3 占用16个字节,但因为是结构体嵌套的内存存储,所以我们要从s3中成员最大对齐数的整数倍处开始存储,s3中最大对齐数的整数倍为8,所以我们从8的位置开始往下存储16个字节,直到23
第三个结构体内容为double d,结构体成员 d 占用8个字节,因为结构体内存对齐原则,占用8个字节大小的double d 只能从8 的整数倍数处(也叫对齐数)开始存储内存,所以我们从 8 的整数倍处开始存储8个字节,也就是从24的位置开始,向下存放8个字节大小的内存
结构体字节的大小:
在我们清楚完毕结构体内存的存储位置后,因为结构体的总大小为最大对齐数(结构体中每个成员都有一个对齐数,所有对齐数中最大的)的整数倍,在此例题中结构体成员的最大对齐数为8,也就是double d 或者 s3中double d 的对齐数 8,
因为结构体内存存储占用了32个字节的大小,所以我们取最大对齐数 8 的整数倍数32,最终大小被填充到32个字节
10.为什么存在内存对齐?
大部分的参考资料都是这样说的:
1.平台原因(移植原因):
不是所有的硬件平台都能访问任意地址上的任意数据的:某些硬件平台只能在某些地址处取某些特定类型的数据,否则抛出硬件异常。
2.性能原因:
数据结构(尤其是栈)应该尽可能地在自然边界上对齐。原因在于,为了访问未对齐的内存,处理器需要作两次内存访问;而对齐的内存访问仅需要一次访问。假设一个处理器总是从内存中取8个字节,则地址必须是8的倍数。如果我们能保证将所有的double类型的数据的地址都对齐成8的倍数,那么就可以用一个内存操作来读或者写值了。否则,我们可能需要执行两次内存访问,因为对象可能被分放在两个8字节内存块中。
总体来说:结构体的内存对齐是拿空间来换取时间的做法。
11.如何节省结构体内存大小
想要节省结构体内存的大小,我们需要同时满足对齐,又要节省空间
我们可以通过将占用空间小的结构体成员尽量集中在一起
例如:
struct S1 { char c1; int i; char c2; }; //S1占用12个字节 struct S2 { char c1; char c2; int i; }; //S2占用8个字节
S1和S2的结构体成员一模一样,但是S2占用的字节比S1小
12.修改默认对齐数
在结构体中,我们可以使用一个预处理指令来修改结构体的默认对齐数
#pragma
#pragma这个预处理指令可以改变编译器的默认对齐数,这对于节省结构体内存非常有用
#pragma改变默认对齐数后,所有结构体内容的对齐数都会被#pragma修改
使用示例如下:
#pragma pack(1)//设置结构体默认对齐数为1 struct S { char c1; int i; char c2; }; struct S1 { double d; char c; int i; }; #pragma pack()//恢复结构体默认对齐数 printf("%d\n", sizeof(struct S)); //打印6 printf("%d\n", sizeof(struct S1)); //打印13
修改结构体默认对齐数后,所有结构体成员都会受影响,因此结构体成员的最大对齐数也会随之改变
分析如图:
因为结构体成员的最大对齐数为1,所以结构体内存就以最大对齐数的整数倍(也就是 1 的整数倍)为结构体字节大小
13.scand初始化结构体变量
使用scanf初始化结构体变量,就需要对变量内的内容直接访问,以初始化结构体变量内的内容
struct stu { int a; char arr[20]; }s1; scanf("%d%s", &s1.a, s1.arr); //s1.arr,代表的是s1中arr数组首元素的地址,所以不需要使用 & 操作符 //输入20 abc printf("%d %s", s1.a, s1.arr); //打印20 abc
错误写法如下:
使用&s1是不可以完成赋值结构体变量的内容的,因为结构体变量内存的存储方式多种多样,结构体变量的地址与结构体变量第一个内容的地址相同,但之后的变量内容怎么办?无法通过scanf一次性输入
所以不能使用&s1来实现结构体的输入
14.结构体传参
结构体传参传的是什么?
结构体传参有两种传法,一种是将整个结构体内容全部传过去,另一种则是将结构体变量的地址传过去
示例如下:
#define _CRT_SECURE_NO_WARNINGS #include<stdio.h> struct stu //结构体类型为struct stu { int a; char arr[20]; int d; }s1 = { 20,{"w"},98 }; void print1(struct stu s1) { printf("%d %c %d\n", s1.a, s1.arr[0], s1.d); //打印 20 w 98 } void print2(struct stu* ps) { printf("%d %c %d\n", ps->a, ps->arr[0], ps->d); //打印 20 w 98 } int main() { print1(s1); print2(&s1); }
注意:结构体传参时,结构体必须为全局变量,如果为局部变量,结构体传参就会报错,如下图:
问:print1 与 print2 哪个更好些?
答案是首选print2函数,传递结构体指针的地址更好
原因:函数传参时,参数是需要压栈的,会有时间和空间上的开销
如果我们将一整个结构体传过去,结构体的内存越大,参数压栈的系统开销就越大,会导致性能的下降
结论:结构体传参时,首选传递结构体的地址