前言
关于C语言,在指针学习过程中,在学习之余,做了一次总结,写成了一篇详解博客,也向大家分享的我对于指针的见解,我发现将所学的内容写成博客,不仅仅可以使和我一样的初学者们更快的了解相关知识点,还可以让我查缺补漏,弥补自己的短板,让我的基础更加扎实,所以在学完结构体以后,我也将结构体的内容整理成一篇博客,向大家分享。
为什么要学习结构体
当我们需要表达一个数据的时候,我们就需要用到变量,而变量又需要定义一个类型。我们通过之前的学习,知道了C语言中变量类型有:int、double、char、float等等基础类型,还有指针等等。但是如果我们想表达的数据比较复杂,不是一个数据,例如:日期(年、月、日)、学生信息(姓名、性别、年龄等等)、时间(时、分、秒)等等。而我们又想用一个整体来表达这些所有的数据,这个时候我们就需要用到一个自定义变量类型——结构体。
什么是结构体
结构体是C语言中一种重要的数据类型,该数据类型由一组称为成员(或称为域,或称为元素)的不同数据组成,其中每个成员可以具有不同的类型。结构体通常用来表示类型不同但是又相关的若干数据。
注意:结构体是一种数据类型!!!
一、结构体:struct
1、结构体类型的声明
(1)结构体的基础知识
成员变量:结构是一些值的集合,这些值称为成员变量。
结构体的每个成员可以是不同类型的变量。
(2)结构体的声明
结构体的定义如下所示,struct为结构体关键字,tag为结构体的标志,member-list为结构体成员列表,其必须列出其所有成员;variable-list为此结构体声明的变量。
struct tag
{
member-list
}variable-list;
例1:描述一个学生信息
#include<stdio.h>
struct Stu
{
char name[20];
char tele[12];
char sex[5];
int age;
}s4,s5,s6;
struct Stu s3;
int main()
{
struct Stu s1;
struct Stu s2;
return 0;
}
易错提示: 一定不要忘记结束时的分号!!!
(3)特殊的声明
在声明结构的时候,可以不完全的声明 例2:匿名结构体类型
#include<stdio.h>
struct
{
int a;
char b;
float c;
}x;
struct
{
int a;
char b;
float c;
}* p;
int main()
{
p = &x;
return 0;
}
上面的两个结构在声明的时候省略了结构体的标志(tag)。(是正确的)
上述代码的问题出现在:p = &x。//这是非法的。 上面的两个匿名结构体虽然各自的成员是一模一样的,但是在编译器看来,它们是两个不同的类型,所以出现了报错(部分编译器是警告)。
2、结构体的自引用
结构体的自引用就是指在结构体内部,包含指向自身类型结构体的指针。
在之前的学习中,我们知道了在函数中可以包含自己(即递归),那么在结构中包含一个类型为该结构体本身的成员是否可以呢?
例3:
#include<stdio.h>
struct Node
{
int data;
struct Node n;
};
int main()
{
sizeof(struct Node);
return 0;
}
运行结果: 错误 C2460 “Node::n”: 使用正在定义的“Node”
这是为什么呢?因为n定义中又有n,无限循环,系统无法确定该结构体的长度,会判定定义非法。
切记,结构体自引用,成员定义只能是指针。
例4:正确的自引用
#include<stdio.h>
struct Node
{
int data;
struct Node* n;
};
int main()
{
int a = sizeof(struct Node);
printf("%d", a);
return 0;
}
运行结果为:8(因为博主是32位)
这是为什么呢?因为我们在自引用时把结构的成员定义为指针,又指针的长度是确定的(上一节指针详解中提到过),所以此时结构体的长度也是确定的。
这时候可能有人又有疑问了,我们刚刚学了一种特殊的声明方式——匿名,那在自引用的时候,我们可不可以使用匿名了,就拿例4来举例子,因为这时候,我们使用了匿名结构,所以里边使用“Node* n;”不就好了吗?我们来看看,结构是怎样的: 例5:
#include<stdio.h>
struct
{
int data;
Node* n;
}Node;
int main()
{
int a = sizeof(struct Node);
printf("%d", a);
return 0;
}
运行结果为:一大堆报错
这是为什么呢?因为我们在声明结构体的内部,就使用了Node这个变量,但是我们的编译器是在声明结构体结束以后,才接收到Node这个变量,所以,在使用Node变量的时候,编译器无法识别,就自然会出现错误。
建议:在使用结构体自引用的时候最好不要使用匿名声明结构体。
3、结构体变量的定义和初始化
前面我们了解了如何声明结构体的类型,现在我们有了结构体类型,那么我们要如何定义一个结构体变量以及初始化一个结构体变量呢?其实非常简单。
(1)单一结构体的定义和初始化
例6:
#include<stdio.h>
struct Stu
{
char name[20];
int age;
char sex[5];
}s1;
int main()
{
struct Stu s2 = { "lisi",18,"nan" };
printf("%s %d %s", s2.name[20], s2.age, s2.sex[5]);
return 0;
}
运行结果为: lisi 18 nan PS:这里博主用的是Visual Studio 2019
通过struct+结构体的标志(tag)+变量名,就完成了结构体的定义;而在{}内把结构体成员对应的类型用逗号隔开赋值给声明的结构体,我们就完成的结构体的初始化。
(2)嵌套结构体的定义和初始化
刚刚我们了解了结构体的自引用,了解了结构体内是可以存在结构体的,也就是结构体的嵌套,现在我们了解了单一的结构体如何定义和初始化,那有人就会想了,嵌套结构体如何进行定义和初始化呢? 例7:
#include<stdio.h>
struct T
{
int c;
double weight;
};
struct Stu
{
char name[20];
struct T p;
int age;
char sex[5];
};
int main()
{
struct Stu s = { "lisi",{30,1.0},18,"nan"};
printf("%d %lf",s.p.c,s.p.weight);
return 0;
}
运行结果为:30 1.000000
在结构体中遇到结构体,我们在初始化的时候,同样的方法在外层结构体的{}内再添加一个{}即可。
注意:嵌套结构体在调用的时候,逐层调用。
4、结构体内存对齐
通过前面的学习,我们已经掌握了结构体的基本使用了。
有人就又会问了,结构体是变量,那变量就有大小啊,我们如何计算结构体的大小呢?
这里就涉及到了一个热门的考点:结构体内存对齐。
(1)单一结构体内存对齐
先来做一道练习题: 例8:
#include<stdio.h>
struct s1
{
char c1;
int a;
char c2;
};
struct s2
{
char c1;
char c2;
int a;
};
int main()
{
struct s1 s1 = { 0 };
printf("%d\n", sizeof(s1));
struct s2 s2 = { 0 };
printf("%d\n", sizeof(s2));
return 0;
}
运行结果为: 12 8
大家第一次拿到这个题,肯定会想:这有什么好算的,不就是6、6吗?但是结构体的大小计算不是这样随便计算的,它需要符合一定的条件。
那么到底如何计算呢?我们需要利用结构体对齐规则: ①第一个成员在与结构体变量偏移量为0的地址处。 ②其他成员变量要对齐到某个数字(对齐数)的整数倍的地址处。 对齐数 = 编译器默认的一个对齐数 与 该成员大小的较小值 提示:VS中默认的值为8 ③结构体总大小为最大对齐数(每个成员变量都有一个对齐数)的整数倍。
看完对齐规则,我们回到例8 ㈠先看到struct s1这个结构体 第①步 结构体存放变量从偏移量为0的位置开始: 就是从图中0开始,又因为c1是一个字节,所以c1就是0 ~ 1。
第②步 其他成员变量要对齐到某个数字(对齐数)的整数倍的地址处: 对齐数 = 编译器默认的一个对齐数 与 该成员大小的 较小值 a的对齐数为4,所以a要放到4的整数倍的地址处,所以a从4开始,又因为a是4个字节,所以a就是4 ~ 8; 到这里有人就会问了:那中间的1 ~ 4怎么办,中间这部分就浪费掉了。 c2的对齐数为1,c2是1个字节,所以从8开始,c2是8~9。
第③步 结构体总大小为最大对齐数(每个成员变量都有一个对齐数)的整数倍。 对于这个结构体中的成员: c1的对齐数为1,a的对齐数为4,c2的对齐数为1,那么最大对齐数就是4; 而现在我们的一共用了9个字节,9不是4的整数倍,所以我们还要再浪费3个字节,达到4的整数倍12个字节。
㈡再看到struct s2这个结构体
第①步 结构体存放变量从偏移量为0的位置开始: 就是从图中0开始,又因为c1是一个字节,所以c1就是0 ~ 1。
第②步 其他成员变量要对齐到某个数字(对齐数)的整数倍的地址处: 对齐数 = 编译器默认的一个对齐数 与 该成员大小的 较小值 c2的对齐数为1,所以c2要放到1的整数倍的地址处,所以c2从1开始,又因为c2是1个字节,所以c2就是1 ~ 2; a的对齐数为4,所以a要放到4的整数倍的地址处,所以a从4开始,又因为a是4个字节,所以a就是4 ~ 8。
第③步 结构体总大小为最大对齐数(每个成员变量都有一个对齐数)的整数倍。 对于这个结构体中的成员: c1的对齐数为1,c2的对齐数为1,a的对齐数为4,那么最大对齐数就是4; 现在我们一共用了8个字节,8是4的整数倍,所以这个结构体的大小就是8个字节。
趁热打铁,再来一道练习题: 例9:
#include<stdio.h>
struct s3
{
double a;
char b;
int c;
};
int main()
{
printf("%d\n", sizeof(struct s3));
return 0;
}
运行结果为:16
你做对了吗?如果没做对,没关系重新来过,再温习一遍例题;如果做对了,是不是成就感满满,但是别急,下面还有更难的!
(2)嵌套结构体内存对齐
在思考每一个问题的同时,不要忘记我们学过的结构体是可以嵌套的。但是不要担心,我们的对齐规则考虑到了这种情况:
④如果嵌套了结构体的情况,嵌套的结构体对齐到自己的最大对齐数的整数倍处,结构体的整体大小就是所有最大对齐数(含嵌套结构体的对齐数)的整数倍。
来看一道例题: 例10:
#include<stdio.h>
struct s3
{
double a;
char b;
int c;
};
struct s4
{
char c1;
struct s3 s3;
double d;
};
int main()
{
printf("%d\n", sizeof(struct s4));
return 0;
}
运行结果为:32
还是按照步骤来解题: 第①步 结构体存放变量从偏移量为0的位置开始: 就是从图中0开始,又因为c1是一个字节,所以c1就是0 ~ 1。
第②步 其他成员变量要对齐到某个数字(对齐数)的整数倍的地址处, 如果嵌套了结构体的情况,嵌套的结构体对齐到自己的最大对齐数的整数倍处: 对齐数 = 编译器默认的一个对齐数 与 该成员大小的 较小值 对于struct s3这个结构体: a的对齐数为8,b的对齐数为1,c的对齐数为4,所以最大对齐数为8; 所以s3要放到8的整数倍的地址处,所以s3从8开始,又因为s3是16个字节(例9),所以s3就是8~24; d的对齐数为8,所以a要放到8的整数倍的地址处,所以a从24开始,又因为a是8个字节,所以a就是24 ~ 32。
第③步 结构体的整体大小就是所有最大对齐数(含嵌套结构体的对齐数)的整数倍。 对于这个结构体中的成员: c1的对齐数为1,s3的对齐数为8,d的对齐数为8,那么最大对齐数就是8; 现在我们一共用了32个字节,32是4的整数倍,所以这个结构体的大小就是32个字节。
(3)对齐规则
①第一个成员在与结构体变量偏移量为0的地址处。 ②其他成员变量要对齐到某个数字(对齐数)的整数倍的地址处。 对齐数 = 编译器默认的一个对齐数 与 该成员大小的较小值 提示:VS中默认的值为8 ③结构体总大小为最大对齐数(每个成员变量都有一个对齐数)的整数倍。 ④如果嵌套了结构体的情况,嵌套的结构体对齐到自己的最大对齐数的整数倍处,结构体的整体大小就是所有最大对齐数(含嵌套结构体的对齐数)的整数倍。
(4)为什么存在内存对齐
大家在对于对齐规则的学习中,肯定会有这样的疑问: 我们在对齐的过程中,浪费了那么多空间,那为什么还要存在内存对齐呢?
①平台原因(移植原因):不是所有的硬件平台都能访问任意地址上的任意数据的;某些硬件平台只能在某些地址处取某些特定类型的数据,否则抛出硬件异常。 ②性能原因:数据结构(尤其是栈)应该尽可能地在自然边界上对齐。原因在于:为了访问未对齐的内存,处理器需要作两次内存访问;而对齐的内存访问仅需要一次访问。
即 结构体的内存对齐是拿空间来换取时间的做法。
(5)如何利用内存对齐
前面我们了解到:内存对齐是拿空间来换取时间的做法。
那么我们如何做到既要满足内存对齐,又要节省空间呢? 让占用空间小的成员尽量集中在一起。
举个例子: 例11:
struct s1
{
char c1;
int a;
char c2;
};
struct s2
{
char c1;
char c2;
int a;
};
这里s1和s2类型的成员是一模一样的,但是s2占用的空间比s1小。
(6)修改默认对齐数
在C语言中默认对齐数是可以修改的,利用 #pragma 这个预处理命令,就可以改变默认对齐数。
举一个例子: 例12:
#include<stdio.h>
struct s1
{
char c1;
double a;
};
#pragma pack(4)
struct s2
{
char c1;
double a;
};
#pragma pack()
int main()
{
printf("%d\n", sizeof(struct s1));
printf("%d\n", sizeof(struct s2));
return 0;
}
运行结果为: 16 12
这里可以看到: s1中,存放a的时候对齐数为8,所以a从8开始,又因为a是8个字节,所以a是8~16,所以s2是16个字节; 我们将默认对齐数修改为4的时候,s2中,存放a的时候对齐数为4,所以a从4开始,又因为a是8个字节,所以a是4~12,所以s2是12个字节。
(7)offsetof()函数
offsetof()函数是用来返回结构体成员的偏移量。 使用offsetof()函数时,需要加上 #include<stddef.h> 这个头文件 offsetof(variable-list,member-list)
举个例子: 例13:
#include<stdio.h>
#include<stddef.h>
struct s
{
char c;
int a;
double b;
};
int main()
{
printf("%d\n", offsetof(struct s, c));
printf("%d\n", offsetof(struct s, a));
printf("%d\n", offsetof(struct s, b));
return 0;
}
运行结果为: 0 4 8
5、结构体传参
直接上例子: 例14:
#include<stdio.h>
struct s
{
char c;
int a;
double b;
};
void func1(struct s p)
{
p.a = 100;
p.b = 3.14;
p.c = 'w';
}
void func2(struct s* p)
{
p->a = 100;
p->b = 3.14;
p->c = 'w';
}
void print1(struct s tmp)
{
printf("%d %lf %c\n", tmp.a, tmp.b, tmp.c);
}
void print2(struct s* tmp)
{
printf("%d %lf %c\n", tmp->a, tmp->b, tmp->c);
}
int main()
{
struct s s = { 0 };
func1(s);
print1(s);
print2(&s);
func2(&s);
print1(s);
print2(&s);
return 0;
}
运行结果为: 0 0.000000 0 0.000000 100 3.140000 w 100 3.140000 w
通过例14,我们可以看出func1进行传参,只是形参,func2进行传参,传的是地址;同样print1是传值,而print2是传址。两种传递方法都可以,但是我们更加提倡以地址的形式进行传递,因为这样是以指针的形式传递,无论结构体有多大,指针的大小均为4/8。
6、结构体实现位段(位段的填充&可移植性)
(1)什么是位段
位段的声明和结构是类似的,有两个不同: ①位段的成员必须是int、unsigned int 、 signed int 或 char。 ②位段的成员名后边有一个冒号和一个数字。
来做一道题: 例15:
#include<stdio.h>
struct s
{
int a : 2;
int b : 5;
int c : 10;
int d : 30;
};
int main()
{
struct s s;
printf("%d\n", sizeof(s));
return 0;
}
运行结果为:8
这里大家就会猜测说:2+5+10+30=47bit,那不应该是6个字节吗?为什么是8个字节啊,这是因为位段也有它的规则。
(2)位段的内存分配
①位段的成员可以是int、unsigned int 、 signed int 或 char(属于整形家族)类型。 ②位段的空间上是按照需要以4个字节(int)或者1个字节(char)的方式来开辟的。 ③位段涉及很多不确定因素,位段是不跨平台的,注重可移植的程序应该避免使用位段。
现在我们再来看例15,: a,b,c一共需要17个bit来存放,这时,需要开辟4个字节(32bit)的空间来存放;但是剩下的15个bit不足以存放d,所以就需要再开辟4个字节(32bit)的空间来存放d。(剩余的空间浪费了) 所以共需8个字节。
(3)位段的跨平台问题
①int位段被当成有符号数还是无符号数是不确定的; ②位段中最大位的数目不能确定;(16位机器最大16,32位机器最大32) ③位段中的成员在内存中从左向右分配,还是从右向左分配标准尚未定义; ④当一个结构包含两个位段,第二个位段成员比较大,无法容纳于第一个位段剩余的位时,是舍弃剩余的位还是利用,这是不确定的。
(4)比较
跟结构体相比,位段可以达到同样的效果,但是可以很好的节省空间,但是有跨平台的问题存在。
二、枚举:enum
概念:一一列举,把可能的取值一一列举。
1、枚举类型的定义
举一个例子: 例16:一个人的性别
#include<stdio.h>
enum Sex
{
MALE,
FEMALE,
SECRET
};
int main()
{
enum Sex s = MALE;
printf("%d %d %d\n", MALE, FEMALE, SECRET);
return 0;
}
运行结果为:0 1 2
注意:在定义枚举时,我们可以随意定义,但是如果没有赋值,会默认为0,1,2,……,同时枚举作为一个常量,我们无法在定义完成后进行修改。
2、枚举的优点
①增加代码的可读性和可维护性; ②和 #define 定义的标识符比较枚举,枚举具有类型检查,更加严谨; ③防止了命名污染(封装); ④便于调试; ⑤使用方便,一次可以定义多个常量。
3、枚举的使用
再举一个例子: 例17:
#include <stdio.h>
enum DAY
{
MON = 1, TUE, WED, THU, FRI, SAT, SUN
};
int main()
{
enum DAY day;
day = MON;
printf("%d", day);
return 0;
}
运行结果为:1
三、联合体(共用体):union
1、联合体类型的定义
联合体也是一种特殊的自定义类型。这种类型定义的变量也包含一系列的成员,特征是这些成员公用同一块空间(所以联合也叫共用体)。
来看一个例子 例18:
#include<stdio.h>
union un
{
char c;
int i;
};
int main()
{
union un u;
printf("%d\n", sizeof(u));
printf("%p\n", &u);
printf("%p\n", &(u.c));
printf("%p\n", &(u.i));
}
运行结果为: 4 004FFD3C 004FFD3C 004FFD3C
这也说明了:联合体的成员公用同一块空间。
2、联合体的特点
联合体的成员是公用同一块内存空间的,这样一个联合变量的大小,至少是最大成员的大小(因为联合体至少得有能力保存最大的那个成员)。
3、联合体大小的计算
前面的结构体和枚举都有自己的规则,那联合体也不例外: ①联合体的大小至少是最大成员的大小; ②当最大成员大小不是最大对齐数的整数倍的时候,就要对齐到最大对齐数的整数倍。
练习1
判断当前计算机的大小端存储。 例19:
#include<stdio.h>
int check_sys()
{
int a = 1;
return *(char*)&a;
}
int main()
{
int a = 1;
int ret = check_sys();
if (1 == ret)
{
printf("小端\n");
}
else
{
printf("大端\n");
}
return 0;
}
运行结果为:小端
练习2
制作学生管理系统
这个博主现在正在研究,也欢迎大家来交流,因为目前能力所限,这个到时候会再写一篇博客,专门说明这个。
总结
不知不觉,结构体的内容已经结束了,博主从晚上七点奋战至凌晨四点,不得不感叹道:时间过的真快的。学习的时光总是美好的,每天能学到新的知识就会感到很充实,做这个博客的原因不光是想查缺补漏一下,更多的是想帮助那些初学者,让他们能够很快理解这些知识点,一起加油。
|