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


前言

关于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;//4
	struct Node n;//4+套娃
};

int main()
{
	sizeof(struct Node);
	return 0;
}

运行结果:
错误 C2460 “Node::n”: 使用正在定义的“Node”

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

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

例4:正确的自引用

#include<stdio.h>
struct Node
{
	int data;//4
	struct 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 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)
//设置默认对齐数为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;//2个bit
	int b : 5;//5个bit
	int c : 10;//10个bit
	int 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

制作学生管理系统

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

总结

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

  • 6
    点赞
  • 59
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 2
    评论
评论 2
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

WE-ubytt

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值