100字范文,内容丰富有趣,生活中的好帮手!
100字范文 > C语言自定义变量类型——结构体详解

C语言自定义变量类型——结构体详解

时间:2020-08-22 02:16:52

相关推荐

C语言自定义变量类型——结构体详解

文章目录

前言为什么要学习结构体什么是结构体一、结构体:struct1、结构体类型的声明(1)结构体的基础知识(2)结构体的声明(3)特殊的声明2、结构体的自引用3、结构体变量的定义和初始化(1)单一结构体的定义和初始化(2)嵌套结构体的定义和初始化4、结构体内存对齐(1)单一结构体内存对齐(2)嵌套结构体内存对齐(3)对齐规则(4)为什么存在内存对齐(5)如何利用内存对齐(6)修改默认对齐数(7)offsetof()函数5、结构体传参6、结构体实现位段(位段的填充&可移植性)(1)什么是位段(2)位段的内存分配(3)位段的跨平台问题(4)比较二、枚举:enum1、枚举类型的定义2、枚举的优点3、枚举的使用三、联合体(共用体):union1、联合体类型的定义2、联合体的特点3、联合体大小的计算练习1练习2总结

前言

关于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;//s3,s4,s5,s6为全局变量int main(){//创建结构体变量struct Stu s1;struct Stu s2;//s1,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;//错误 E0513不能将 "struct <unnamed> *" 类型的值分配到 "struct <unnamed> *" 类型的实体return 0;}

上面的两个结构在声明的时候省略了结构体的标志(tag)。(是正确的)

上述代码的问题出现在:p = &x。//这是非法的。

上面的两个匿名结构体虽然各自的成员是一模一样的,但是在编译器看来,它们是两个不同的类型,所以出现了报错(部分编译器是警告)。

2、结构体的自引用

结构体的自引用就是指在结构体内部,包含指向自身类型结构体的指针。

在之前的学习中,我们知道了在函数中可以包含自己(即递归),那么在结构中包含一个类型为该结构体本身的成员是否可以呢?

例3:

#include<stdio.h>struct Node{int data;//4struct Node n;//4+套娃};int main(){sizeof(struct Node);return 0;}

运行结果:

错误 C2460 “Node::n”: 使用正在定义的“Node”

这是为什么呢?因为n定义中又有n,无限循环,系统无法确定该结构体的长度,会判定定义非法。

切记,结构体自引用,成员定义只能是指针。

例4:正确的自引用

#include<stdio.h>struct Node{int data;//4struct Node* n;//4/8};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

通过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)//设置默认对齐数为4struct 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;//2个bitint b : 5;//5个bitint c : 10;//10个bitint d : 30;//30个bit};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;//返回1表示小端//返回0表示大端return *(char*)&a;}int main(){int a = 1;int ret = check_sys();if (1 == ret){printf("小端\n");}else{printf("大端\n");}//int a = Ox11223344;////低地址---------------------------------------------->高地址//...[][][][11][22][33][44][][][][]...大端字节序存储模式//...[][][][44][33][22][11][][][][]...小端字节序存储模式//讨论一个数据,放在内存中的存放的字节顺序//大小端字节序问题//return 0;}

运行结果为:小端

练习2

制作学生管理系统

这个博主现在正在研究,也欢迎大家来交流,因为目前能力所限,这个到时候会再写一篇博客,专门说明这个。

总结

不知不觉,结构体的内容已经结束了,博主从晚上七点奋战至凌晨四点,不得不感叹道:时间过的真快的。学习的时光总是美好的,每天能学到新的知识就会感到很充实,做这个博客的原因不光是想查缺补漏一下,更多的是想帮助那些初学者,让他们能够很快理解这些知识点,一起加油。

本内容不代表本网观点和政治立场,如有侵犯你的权益请联系我们处理。
网友评论
网友评论仅供其表达个人看法,并不表明网站立场。