【C语言】结构体(struct)最全的讲解(万字干货)以及枚举类型

谢谢观看!希望以下内容帮助到了你,对你起到作用的话,可以一键三连加关注!你们的支持是我更新地动力。
因作者水平有限,有错误还请指出,多多包涵,谢谢!



  在自定义的类型中一共有3种: 结构体联合体枚举
  那么就先让我们了解一下结构体的细节枚举类型吧。


一、结构体类型的声明

1.1结构体回顾

  结构是一些值的集合,这些值称为成员变量。结构的每个成员可以是相同类型的变量,也可以是不同类型的变量。成员变量可以是一个或者多个。
  数组也是一些值的集合。数组的每个元素只能是相同类型的数据。元素个数可以是一个或者多个。
  注意区分数组和结构的不同点和相同点
  

1.1.1结构的声明

struct tag
{
 member-list;//成员列表
}variable-list;//变量列表

  让我们通过代码来描述一个学生:一个学生具有的属性是年龄、名字、性别、学号等

//结构体类型,类似于int,char,float等类型
//不同的是,这个是我们自己创建的类型,我们创建类型是为了什么?当然是为了创建变量了,类似于int a、 char c等
//类型名是struct Stu,和int 、char一样
struct Stu
{
 	char name[20];//名字
 	int age;//年龄
 	char sex[5];//性别
 	char id[20];//学号
}; //分号不能丢,重要的部分

  

1.1.2结构体变量的创建和初始化

结构体变量的创建2种方式,但是有点小区别

  • 声明的时候进行创建结构体变量。此时的变量是全局变量
  • main()函数中用结构体类型来创建结构体变量。此时的变量是此时的变量是局部变量
struct Stu
{
 	char name[20];//名字
 	int age;//年龄
 	char sex[5];//性别
 	char id[20];//学号
}s1,s2;结构体变量的创建第一种方法,创建了变量s1、s2,类型是struct Stu
//s1、s2是全局变量
int main()
{
	//结构体变量的创建第二种方法
	struct Stu s3;//创建了变量s3,类型是struct Stu
	struct Stu s4;//创建了变量s4,类型是struct Stu
	struct Stu s5;//创建了变量s5,类型是struct Stu
	//s3、s4、s5是局部变量

	struct Stu s6 = {"王五",18,"男","2024"};//在创建结构体变量是进行赋值,就是初始化
	return 0;
}

结构体变量的初始化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);
 
 //按照指定的顺序初始化
 //用`{.成员变量 = , .成员变量 = }`(使用到`.`操作符)来初始化变量
 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;
}

  通过上面学习结构体的知识,让我们写个代码实现一下,创建和初始化,以及打印数据

struct Book
{
	char book_name[20];//书的名字
	char author[20];//作者
	float price;//定价
	char id[19];//书号
};

int main()
{
	struct Book b1 = { "C语言","王五",38.8f,"PG20240520" };
	struct Book b2 = { .id="PG20240510",.book_name="c++",.author="李四",.price=55.5f };
	printf("%s %s %f %s\n", b1.book_name, b1.author, b1.price, b1.id);
	printf("%s   %s %f %s\n", b2.book_name, b2.author, b2.price, b2.id);
	return 0;
}

在这里插入图片描述
  
  补充知识:可以看到红色方框和红色箭头,我们发现此时打印的浮点数类型的数据居然有偏差。其实这就说明了一点:浮点数在内存中有可能是不能精确保存的

  分析一下:对于5.59.0我们是可以准确的写成对应的2进制数1001.0101.1,那么对于这样的浮点数,在内存中是可以准确存放相关的值(E、M、S),但是对于38.8,我们是不能完整地写成对应的2进制数据100110.110...,因为0.8=0.5+0.25+...会始终差一些才可以等于0.8,而由于对于float浮点数中的M相关值的存放限制在23位,那么就可能会舍弃掉一部分的位数,导致浮点数在内存中并不是准确保存的
  

在这里插入图片描述

  所以结论是浮点数在内存中并不是准确保存的。那根据这一点,在我们对浮点数进行比较大小时,我们就不能简单地对2个数据进行 ==是否相等的判断,而是有一定的误差

int main()
{
	//这是错误的比较浮点数的大小,因为可能会导致一些误差
	flaot f = 3.45;
	if(f == 3.45)
	{
	}

	//这是正确的比较浮点数的大小
	if(fabs(f-3.45) < 0.0000001)//0.0000001为自己可以接受的最大误差值,abs()是求绝对值的函数,fabs()是对浮点数求绝对值的函数
	{}//相等
	else
	{}//不相等
	return 0;
}

  

1.2结构的特殊声明

  在声明结构时,可以不完全声明

//匿名结构体类型,不完全声明
struct
{
	char c;
	int i;
	double d;
}s1,s2;//匿名结构体类型创建变量的唯一方式,就是在变量列表中创建

  注意:使用匿名结构体类型创建变量就只能在变量列表中创建,而且只能一次,不能在main()函数中创建变量了

//代码一
struct
{
	char c;
	int i;
	double d;
}s1;//s1是结构体变量

struct
{
	char c;
	int i;
	double d;
}* ps;//ps是结构体指针,结构体指针类型为struct{char c;int i;double d;}*

int main()
{
	ps = &s1;//程序会报错,因为两边的指针类型程序会认为是不一样
	return 0;
}
//代码二
int main()
{
	char arr[] = "abcdefg";
	int* p ;
	p = &arr[0];//程序不会报错,而且p和&arr[0]的指针类型是不一样的,p的类型是int*,&arr[0]的类型是char*
	return 0;
}

  通过上面的两个代码,代码一代码二,我们可以得出:在匿名结构体类型中,不要用匿名结构体指针去指向匿名结构体变量(2个匿名结构体的成员变量一样)
  

1.3结构的自引用

  在结构中包含一个类型为该结构本身的成员是否可以呢?

  比如,定义一个链表的节点(数据结构有关的知识

//错误的定义
struct Node//定义了一个节点。节点本身要包涵自己的数据,和指向下一节点
{
 	int data;
 	struct Node next;//“next”使用未定义的 struct“Node”,这是错误的写法
};

  在定义一个链表的节点时,上面是错误的。我们换一个角度看,这个结构体类型的大小我们是否可以得出来,通过sizeof(struct Node)分析可以知道因为一个结构体中再包含一个同类型的结构体变量,这样会使得结构体的大小无穷大,这是不合理的。

  正确的定义如下:

//正确的定义
struct Node//定义了一个节点。节点本身要包涵,自己的数据和指向下一节点的指针
{
 	int data;//数据域,大小为4个字节
 	struct Node* next;//指针域,大小为4或8个字节
};

  此时结构体的大小是可以计算出来的,sizeof(struct Node)=812字节。在代码中,我们是通过结构体指针指向了下一个节点,这样我们就可以对在内存中不是连续存放的数据进行增删查找等操作

  在结构体自引用使用的过程中,夹杂了 typedef 对匿名结构体类型重命名,也容易引入问题,看看下面的代码,可行吗?

typedef struct Node
{
 	int data;
 	Node* next;
}Node;

  答案是:不可行的。因为我们使使用typedefstruct Node类型重命名为Node,是先有了struct Node才有的Node,但是我们在声明类型的时候就已经使用了Node,这是错误的,要搞清楚主和次的关系

  应对写成下面这种:不要将struct Node* next改为了Node* next

typedef struct Node
{
 int data;
 struct Node* next;
}Node;

二、结构体内存对齐

我们已经掌握了结构体的基本使用了。
现在我们深入讨论⼀个问题:计算结构体的大小。
这也是⼀个特别热门的考点: 结构体内存对齐

  首先让我们看一下对齐现象,然后引出对结构体内存对齐的深入理解。

struct S1
{
	char c1;//1个字节
	char c2;//1个字节
	int n;//4个字节
	//一共有6个字节空间
};

struct S2
{
	char c1;//1个字节
	int n;//4个字节
	char c2;//1个字节
	//一共有6个字节空间
};

int main()
{

	printf("%zd\n", sizeof(struct S1));//结果为8,结果不为6,
	printf("%zd\n", sizeof(struct S2));//结果为12//结果既不是6,也不是8
	//可以看到二个结构声明中,顺序不一样了,导致计算结构体类型大小时也不一样
	return 0;
}

  通过上面的代码,可以得出结论:结构体的成员在内存中是存在对齐现象的,称为结构体内存对齐

  补充知识:偏移量是指一个数据和某另一个数据位置的距离,单位是字节。

  其实我们是可以通过代码来大概确认一下为什么是812的结果。此时要使用的C语言提供的宏offsetof计算结构体成员相较于结构体变量起始位置的偏移量,头文件<stddef.h>

offsetof(type,member);//type是结构体类型,member是成员名字,会返回偏移量大小
#include<stddef.h>
struct S1
{
	char c1;//1个字节
	char c2;//1个字节
	int n;//4个字节
	//一共有6个字节空间
};

struct S2
{
	char c1;//1个字节
	int n;//4个字节
	char c2;//1个字节
	//一共有6个字节空间
};

int main()
{

	struct S1 s1 = { 0 };
	printf("%zd\n", offsetof(struct S1, c1));//0
	printf("%zd\n", offsetof(struct S1, c2));//1
	printf("%zd\n", offsetof(struct S1, n));//4
	return 0;
}

在这里插入图片描述
在这里插入图片描述
  
  通过代码,我们可以看出结构体成员相较于结构体变量起始位置的偏移量具体是多少了,那么我们的想法是就可以根据偏移量的大小来画出其结构体成员在内存中的存放形式。对于结构体类型strcut S1,可以看到大小为1+1+2+4=8个字节大小,所以对于按我们自己的想法来理解结构体类型strcut S1我们就没有疑问为什么是8字节了。

  那么让我们再来测试一下结构体类型strcut S2

#include<stddef.h>
struct S1
{
	char c1;//1个字节
	char c2;//1个字节
	int n;//4个字节
	//一共有6个字节空间
};

struct S2
{
	char c1;//1个字节
	int n;//4个字节
	char c2;//1个字节
	//一共有6个字节空间
};

int main()
{

	struct S1 s2 = { 0 };
	printf("%zd\n", offsetof(struct S2, c1));//0
	printf("%zd\n", offsetof(struct S2, n));//4
	printf("%zd\n", offsetof(struct S2, c2));//8
	return 0;
}

在这里插入图片描述
在这里插入图片描述
  
  我们发现此时,按我们自己的想法来理解结构体类型strcut S2,我们又产生新疑问了,画出成员在内存中的存放为9个字节,代码运行的是12个字节,那么我们自己的想法其实是不全对的。

  至于是为什么?这就需要引出对齐规则

2.1对齐规则

首先得掌握结构体的对齐规则

  1. 结构体的第一个成员对齐到和结构体变量起始位置偏移量为0的地址处
  2. 其他成员变量要对齐到某个数字(对齐数)整数倍偏移量处的地址处。
    对齐数 = 编译器默认的⼀个对齐数该成员变量大小的较小值
    • VS 中默认的值为 8
    • Linux中 gcc 没有默认对齐数,对齐数就是成员自身的大小
  3. 结构体总大小为最大对齐数(结构体中每个成员变量都有一个对齐数,所有对齐数中最大的)的整数倍
  4. 如果嵌套了结构体的情况,嵌套的结构体成员对齐到自己的成员中最大对齐数的整数倍处,结构
    体的整体大小就是所有最大对齐数(含嵌套结构体中成员的对齐数)的整数倍。

  下面给出练习,试试通过规则来写成结构体类型的大小是多少!

//练习1
struct S1
{
 char c1;
 int i;
 char c2;
};
printf("%d\n", sizeof(struct S1));//结果为12


//练习2
struct S2
{
 char c1;
 char c2;
 int i;
};
printf("%d\n", sizeof(struct S2));//结果为8


//练习3
struct S3
{
 double d;//double的字节大小为8
 char c;
 int i;
};
printf("%d\n", sizeof(struct S3));//16


//练习4-结构体嵌套问题

struct S3
{
 double d;//double的字节大小为8
 char c;
 int i;
};
struct S4
{
 char c1;
 struct S3 s3;
 double d;
};
printf("%d\n", sizeof(struct S4));//32

  下面我自己在纸上写出了解析,但由于不好放在文章里,就生成了个链接
  解析练习1的图片
  解析练习2的图片
  解析练习3的图片
  解析练习4的图片

总结
  计算整个结构体的大小时,先将成员存放到实际对齐数的整数倍的偏移量处,存放完所以成员后,再根据结构体整个大小=最大的实际对齐数的整数倍(最大对齐数就是实际对齐数中最大的一个)。
  特殊:在对特殊成员(在结构体中的结构体)存放时,就只需要找到它自己内部成员中的最大对齐数就是它的实际对齐数

2.2为什么存在内存对齐?

在这里插入图片描述
  
  那么想一想我们在设计结构体的时候,我们如何做到既要满足对齐,又要节省空间的呢?
  答案是让占用空间小的成员尽量集中在一起

struct S1
{
	char c1;
	int i;
	char c2
};

struct S2
{
	char c1;
	char c2;
	int i;
};

  S1S2类型的成员一样,但是S1S2所占用的空间不一样,S2相较于S1所占空间较小

2.3修改默认对齐数

  #pragma这个预处理指令,可以改变编译器的默认对齐数,从而影响到实际对齐数,导致存放到相应偏移量的位置发生改变,使占用空间发生改变。

#include <stdio.h>

#pragma pack(1)//设置默认对⻬数为1
struct S
{
 	char c1;//1 1 1
 	int i;//4 1 1
 	char c2;//1 1 1
};
#pragma pack()//取消设置的对⻬数,还原为默认

int main()
{
 	//输出的结果是什么?
 	printf("%d\n", sizeof(struct S));//结果为6
 	return 0;
}

  结论:结构体在对齐方式不合适的时候,我们可以自己更改默认对齐数,但是默认对齐数最好使用2的多少次方


三、结构体传参

struct S//类型定义在前面
{
 	int data[1000];
 	int num;
};


//结构体传参,相当于值传递,形参是实参的临时拷贝,因为结构体很大,会导致浪费空间和时间
void print1(struct S s)
{
 	printf("%d\n", s.num);
}

//结构体地址传参,相当于地址传递
void print2(const struct S* ps)
{
 	printf("%d\n", ps->num);
}

int main()
{
	struct S s = {{1,2,3,4}, 1000};
	print1(s); //传结构体
 	print2(&s); //传地址
 	return 0;
}

在这里插入图片描述


四、结构体实现位段

  结构体讲完就得讲讲结构体实现位段的能力。

4.1什么是位段?

在这里插入图片描述

struct A1
{
 int _a:2;//数字部分表示变量只占多少个2进制位,也就是占多少比特位
 int _b:5;
 int _c:10;
 int _d:30;
 int _e:50;//这一行是错误的,int类型的比特位数就只有32位,50超出了限制,会报错
};

struct A2
{
 int _a;
 int _b;
 int _c;
 int _d;
};


int main()
{
	printf("%zd\n",sizeof(struct A1));//结果为8
	printf("%zd\n",sizeof(struct A2));//结果为16
	return 0;
}

在这里插入图片描述
  
  通过代码可以看出,位段可以在一定程度上减少空间的浪费,因为假如一个变量age表示一个人的年龄,设定1-100岁,那么给int变量占4个字节就有点浪费空间了,那么我们可以限制一下

4.2位段的内存分配

在这里插入图片描述

//⼀个例⼦
struct S
{
 char a:3;
 char b:4;
 char c:5;
 char d:4;
};
int main()
{
	printf("%zd\n",sizeof(struct S));//结果为3字节,
	//说明此时位段占3个字节大小存放成员数据,还大概表明了位段存放数据的形式
	
	
	//测试一下位段中是如何存放数据的,调试窗口内存中看
	struct S s = {0};
	s.a = 10;
	s.b = 12;
	s.c = 3;
	s.d = 4;
	
	return 0;
}

在这里插入图片描述
  
  上面的猜想和代码运行的结果大小为3个字节是一致的,可能猜想正确,我们还可以测试一下
在这里插入图片描述
  

4.3位段的跨平台问题

在这里插入图片描述

4.4位段的应用

在这里插入图片描述

4.5位段使用的注意事项

在这里插入图片描述

struct A
{
 int _a : 2;
 int _b : 5;
 int _c : 10;
 int _d : 30;
};
int main()
{
 struct A sa = {0};
 scanf("%d", &sa._b);//这是错误的,
 //因为_b和_a共用了一个字节,而一个字节只有一个地址编号,那么直接对_b取地址操作是错误的
 
 //正确的⽰范
 int b = 0;
 scanf("%d", &b);//输入10
 sa._b = b;//虽然不能取地址操作,但是可以赋值
 printf("%d",sa._b);//结果为10
 return 0;
}

五、枚举类型

5.1枚举类型的声明

在这里插入图片描述

enum Sex//性别
{
	//该枚举类型的三种可能取值
	//它们都是常量,被称为枚举常量,默认从0开始,递增+1
 	MALE,//0
 	FEMALE,//1
 	SECRET//2
};

int main()
{
	enum Sex people1 = MALE;//表示第一个人的性别是男性
    enum Sex people2 = FEMALE;//表示第二个人的性别是女性

	printf("%d\n",MALE);//0
	printf("%d\n",FEMALE);//1
	printf("%d\n",SECRET);//2
	return 0;
}


enum Sex//性别
{
	
 	MALE=7,
 	FEMALE=9,
 	SECRET=6
};

int main()
{
	

	printf("%d\n",MALE);//7
	printf("%d\n",FEMALE);//9
	printf("%d\n",SECRET);//6
	return 0;
}


enum Sex//性别
{
	
 	MALE,
 	FEMALE=8,
 	SECRET
};

int main()
{
	

	printf("%d\n",MALE);//0
	printf("%d\n",FEMALE);//8
	printf("%d\n",SECRET);//9
	return 0;
}

  
  结论:从修改了初始值的常量开始后面的枚举常量表示的数字都递增+1,前面的保持不变。

  
  

5.2枚举的优点

在这里插入图片描述
类型检查

  

//写一个计算器-完成整数的加法、减法、乘法、除法
//代码一
enum Option
{
	EXIT,//0
	ADD,//1
	SUB,
	MUL,
	DIV
};

void menu()
{
	printf("**********************************\n");
	printf("****** 1. add    2. sub     ******\n");
	printf("****** 3. mul    4. div     ******\n");
	printf("****** 0. exit              ******\n");
	printf("**********************************\n");
}
int main()
{
	int input = 0;
	do
	{
		menu();
		printf("请选择:>");
		scanf("%d", &input);
		switch (input)
	{
		case SUB:
			break;
		case ADD:
			break;
		case MUL:
			break;
		case DIV:
			break;
		case EXIT:
			printf("退出\n");
			break;
		default:
			printf("选择错误,重新选择\n");
			break;
		}
	} while (input);

	return 0;
}

void menu()
{
	printf("**********************************\n");
	printf("****** 1. add    2. sub     ******\n");
	printf("****** 3. mul    4. div     ******\n");
	printf("****** 0. exit              ******\n");
	printf("**********************************\n");
}
int main()
{
	int input = 0;
	do
	{
		menu();
		printf("请选择:>");
		scanf("%d", &input);
		switch (input)
	{
		case 1:
			break;
		case 2:
			break;
		case 3:
			break;
		case 4:
			break;
		case 0:
			printf("退出\n");
			break;
		default:
			printf("选择错误,重新选择\n");
			break;
		}
	} while (input);

	return 0;
}

  
  对比一下代码一代码二,在我们看代码主函数main()中的分支语句switch,发现两个代码的可读性不一样,看代码一我们可以明确知道哪一部分要实现加减乘除具体的操作,而代码二中我们是根据打印菜单对应下来才写相应的操作的。

评论 2
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值