桃猿三结义:结构枚举联合

Posted 跳动的bit

tags:

篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了桃猿三结义:结构枚举联合相关的知识,希望对你有一定的参考价值。

前言

古有刘备、关羽、张飞桃园三结义
在这里插入图片描述

现有结构、枚举、联合桃猿三结义
在这里插入图片描述

一、结构体

💦 什么是结构体

🔑 官方来说结构体就是一些值的集合,这些值称为成员变量。结构体的每个成员可以是不同类型的变量。说到集合,数组也是集合,但是不同的是数组只能是相同类型元素的集合

💦 结构体类型的声明

------------------------结构体声明样式------------------------
struct tag
{
  member1;
  member2;
} variable-list;
------------------------解释------------------------
▶ struct是结构体关键字
▶ tag是结构体的标签名,是自定义的
▶ struct tag就是结构体类型
▶ {}里面放的是成员列表
▶ variable-list是变量

在这里插入图片描述

struct Book
{
	char name[20];
	int price;
	char id[];	
}b4, b5, b6;//2、创建结构体变量
int main()
{
	//1、创建结构体变量
	struct Book b1;
	struct Book b2;
	struct Book b3;
	return 0;
}

📝 分析:

这里定义了一个结构体类型struct Book,再使用类型创建变量 (两种方法创建结构体变量):

相同的是:

▶ 它们的类型是相同的,都是struct Book

不同的是:

▶ 在main函数内创建的变量b1,b2,b3是局部变量

▶ 在main函数外创建的变量b4,b5,b6是全局变量


------------------------特殊的结构体声明样式------------------------
struct
{
  char a;
  int b;
  double c;
} s;
------------------------解释------------------------
▶ 这个结构体没有标签名tag
▶ 在声明结构体的时候,可以不完全声明
▶ 这种类型的结构体叫做匿名结构体
▶ 使用匿名结构体直接创建变量s

在这里插入图片描述

struct 
{
	char a;
	int b;
	double c;
} s;
struct 
{
	char a;
	int b;
	double c;
} *ps;//使用匿名结构体类型创建一个变量,这个变量是一个指针
int main()
{
	ps = &s;//?
	return 0;
}

❓❔ 两个相同的匿名结构体类型去创建变量 s 和 *ps ,问这里的 ps = &s; 是合法的吗

📐 验证:
在这里插入图片描述
📝分析:

▶ 在编译器看来,虽然结构体的成员是一样的,但是它会认为 s 和 *ps 是两个不同的类型,所以是非法的

▶ 所以可以试想一下,使用匿名结构体去创建变量时,只能用一次

💦 结构体的自引用

❓❔ 结构体成员包含该结构体创建的结构体变量(非指针)

struct N
{
	int a;
	struct N n;//?
};
int main()
{
	struct N n;
	return 0;
}

📝 分析:
假设这种写法是可行的,那么使用struct N去创建一个变量n,请问n的大小是多大?细想一下,你搁这卡bug呢? 这不是无限套娃吗?😵😵

📐 验证:
语法都不支持
在这里插入图片描述

✔ 但是结构体成员可以包含其它结构体创建的结构体变量(嵌套结构体)

struct U
{
	int b;
	int c;
};
struct N
{
	int a;
	struct U n;//?
};
int main()
{
	struct N n;
	return 0;
}

🧿 拓展

❓❔ 什么是数据结构

数据结构指的是数据在内存中存储结构

🧷 举例:如果要存储1 2 3 4 5
在这里插入图片描述
🔎 这里主要了解链表:

在这里插入图片描述

💦 结构体变量的定义和初始化

🎗 创建局部/全局结构体变量并初始化

struct Book
{
	char name[20];
	int price;
	char id[];	
}b4, b5, b6;
int main()
{
	//使用局部变量初始化
	struct Book b1 = { "CSDN", 38, "202306030033" };
	//使用全局变量初始化
	struct Book b4 = { "C语言", 40, "2451176292" };
	return 0;
}

🎗 嵌套结构体的初始化和成员访问

struct Stu
{
	char name[20];
	int age;
};
struct Book
{
	char name[20];
	int price;
	char id[20];	
	struct Stu s;
};
int main()
{
	//初始化
	struct Book b = { "C语言结构体", 40, "133927471", { "小明", 20 } };
	//访问成员
	//使用.
	printf("%s %d %s\\n%s %d\\n", b.name, b.price, b.id, b.s.name, b.s.age);
	//使用->
	struct Book* ps = &b;//定义一个结构体类型的指针指向b的地址 
	printf("%s %d %s\\n%s %d\\n", ps -> name, ps -> price, ps -> id, ps -> s.name, ps -> s.age);
	
	return 0;
}

💦 结构体内存对齐

❓❔ 一个结构体占多大字节呢,是不是直接把一个结构体里每个成员的类型大小加起来呢
在这里插入图片描述

#include<stdio.h>
struct S	
{
	int i;
	char c;
};
int main()
{
	struct S s = { 0 };
	printf("%d\\n", sizeof(s));
	return 0;	
}

❓❔ 假设结构体的大小 = 结构体里每个成员大小之和,那么这里的结果是5个字节

📐 验证:
在这里插入图片描述

✔ 说明对于结构体是如何计算大小有它自己的规则


⚠ 这个规则就是结构体内存对齐

▶ 第1个成员在与结构体变量偏移量为0的地址处

▶ 其它成员变量要对齐到某个数字(对齐数)的整数倍的地址处。对齐数 = 编译器默认的一个对齐数与该成员大小的较小值。
 VS中默认对齐数是8; Linux没有默认对齐数,它是按照自身大小来对齐的

▶ 结构体总大小为最大对齐数(每个成员变量都有一个对齐数)的整数倍

▶ 如果嵌套了结构体的情况,嵌套的结构体对齐到自己的最大对齐数的整数倍,结构体的整体大小就是所有最大对齐数(含嵌套结构体的对齐数)的整数倍

在这里插入图片描述

在这里插入图片描述
在这里插入图片描述
在这里插入图片描述
在这里插入图片描述


⚠ 为什么会存在内存对齐

注:大部分参考资料是这样说的(没有官方具体的说法):

▶ 平台原因(移植原因): 不是所有的硬件平台都能访问任意地址上的任意数据的;某些硬件平台只能在某些地址处取某些特定类型的数据,否则抛出硬件异常。

▶ 性能原因: 数据结构(尤其是栈)应该尽可能地在自然边界上对齐。 原因在于,为了访问未对齐的内存,处理器需要作两次内存访问;而对齐的内存访问仅需要一次访问

▶ 总体来说:结构体的内存对齐就是拿空间换取时间的做法
在这里插入图片描述
人话:

▶ 假设某些硬件平台规定只能在4的倍数地址处去访问数据,那么未来在储存时就必须存储到指 定的位置上,这样才能被访问,所以在存储数据时最好能内存对齐
在这里插入图片描述
▶ 对于内存对齐:b的访问只需要一次;对于非内存对齐b的访问需要二次
在这里插入图片描述


❓❔ 在设计结构体时,如何做到既要满足对齐,又要节省空间

#include<stdio.h>
struct S1 
{
	char c1;
	int i;
	char c2;
};
struct S2
{
	char c1;
	char c2;
	int i;
};
int main()
{
	struct S1 s1 = { 0 };
	struct S2 s2 = { 0 };
	printf("%d\\n", sizeof(s1));//12
	printf("%d\\n", sizeof(s2));//8
	return 0;
}

📝 小结:

s1和s2的类型成员一模一样,但是s1和s2的大小不同。发现让占用空间小的成员尽量集中在一起有助于节省空间


❓❔ 前面有说到VS的默认对齐数是8,能不能自己调整呢

#include<stdio.h>
//默认对齐数是8
struct S1 
{
	char c1;//0
	//1-3
	int i;//4-7
	char c2;//8
	//9-11
};
//修改默认对齐数为2
#pragma pack(2)//始
struct S2 
{
	char c1;//0
	//1
	int i;//2-5
	char c2;//6
	//7
};
#pragma pack()
int main()
{
	printf("%d\\n", sizeof(struct S1));//12
	printf("%d\\n", sizeof(struct S2));//8
	return 0;
}

📝 小结:

结构体在对齐方式不合适的时候,那么我们可以自己调整默认对齐数

💦 offsetof 宏

函数原型和头
在这里插入图片描述
函数的返回值
在这里插入图片描述
函数的功能
在这里插入图片描述
在这里插入图片描述

#include<stdio.h>
#include<stddef.h>
struct S	
{
	char c1;
	int i;
	char c2;
};
int main()
{
	printf("%d\\n", offsetof(struct S, c1);//0
	printf("%d\\n", offsetof(struct S, i);//4
	printf("%d\\n", offsetof(struct S, c2);//8
	return 0;
}

🧿 百度笔试题:写一个宏,计算结构体中某变量相对于首地址的偏移,并给出说明
(注:在后面会了解并附上链接) [传送门正在维修]

💦 结构体传参

❓❔ 结构体传参可以传值也可以传址,问print1(传值)和print2(传址)哪种方式更好

#include<stdio.h>
struct S
{
	int data[1000];
	int num;
};
//初始化
struct S s = { {1, 2, 3, 4}, 1000 };

//传结构体变量
void print1(struct S s)
{
	printf("%d\\n", s.num);
}
//传结构体变量的地址
void print2(struct S* ps)
{
	printf("%d\\n", ps -> num);
}
int main()
{
	print1(s);//传值
	print2(&s);//传址
	return 0;
}

📝 分析:

从效率的角度来分析:

▶ 结构体传值:假设这个结构体足够大,传值时会拷贝一份一样大的结构体,这时会造成空间和时间上的浪费

▶ 结构体传址:不管这个结构体多大,它都是一个地址,无非就是4/8个字节

从功能的角度来分析(这是相对的):

▶ 结构体传值:假设我们希望在对结构体的内容做一些修改,那么传值就有一定的局限性

▶ 结构体传址:而传址可以有权限去对结构体的内容做一些修改;当然从某方面看,它又是不安全的,如果不希望修改其内容,我们可以加上const来限定

小结:

▶ 结构体传参时,要传结构体的地址

💦 结构体实现位段(位段的填充&可移植性)

❓❔ 什么是位段

🔑 位段的声明和结构是类似的

▶ 位段的成员必须是int、unsigned int 或 signed int (注:经测试位段的成员也能是char类型)

▶ 位段的成员名后面有一个冒号和一个数字
在这里插入图片描述
❓❔ 观察分析以下包含位段成员的结构体大小是多大

#include<stdio.h>
struct A
{
	int _a : 2;
	int _b : 5;
	int _c : 10;
	int _d : 30;
};
int main()
{
	printf("%d\\n", sizeof(struct A));//8
	return 0;
}

📝 分析:
int _a : 2; 代表_a这个变量占2个bit位
int _b : 5; 代表_b这个变量占5个bit位

且位段的空间是按照需要4个字节(int)或者1个字节(char)的方式来开辟的

在这里插入图片描述
📝 小结:

位段涉及很多不确定的因素,位段是不跨平台的,注重可移植程序应该避免使用位段


🔎 探讨位段里的内存数据是如何开辟的

struct S
{
	char a : 3;
	char b : 4;
	char c : 5;
	char d : 4;	
};
int main()
{
	struct S s = { 0 };
	s.a = 10;
	s.b = 12;
	s.c = 3;
	s.d = 4;
	return 0;
}

🧷 假设在VS中:当一块空间剩余的空间不够下一个成员使用时,它会浪费掉。且先使用低位的内容再使用高位的内容
在这里插入图片描述
在这里插入图片描述
📝 小结:

经分析验证:在VS中位段的存储模式正如我们上述的假设(注意仅适用于VS)


位段的跨平台问题

▶ int位段被当成有符号数还是无符号数是不确定的

▶ 位段中最大位的数目不能确定(假设是int位段,接着为a分配30个bit的空间 -> int _a : 30,这样写可能有问题,因为在16位平台下,一个int占16bit)

▶ 位段中的成员在内存中从左向右分配,还是从右向左分配标准尚未定义

▶ 当一个结构包含两个位段,第二个位段成员比较大,无法容纳于第一个位段剩余的位时,是舍弃剩余的位还是利用,这是不确定的

📝 小结:

▶ 位段可以很好的节省空间,但是有跨平台的问题


🔎 位段的应用
在这里插入图片描述
以上是一个IP数据包的格式:

数据以上的信息都是为了正确的传输信息,如果这些信息在传输时大小不以限制(比如4位版本号给个整型、4位首部长度给个整型…),🧑发了一百条变态给👦,虽然就两字,但是浪费的资源却是庞大的;如果使用位段(比如4位版本号给4位,4位首部长度给4位…),那么效率会大大的提高

二、枚举

💦 什么是枚举

🔑 官方来说:在数学和计算机科学理论中,一个集的枚举是列出某些有穷序列集的所有成员的程序,或者是一种特定类型对象的计数。这两种类型经常(但不总是)重叠。
简单来说就是一一列举,把可能的取值一一列举

💦 枚举类型的声明和定义

------------------------枚举的声明样式------------------------
enum tag
{
  possible values1,
  possible values2,
  possible values3
} variable-list;
------------------------解释------------------------
▶ enum是枚举关键字
▶ tag是枚举的标签名,是自定义的
▶ enum tag就是枚举类型
▶ {}里面放的是枚举可能取值,也叫枚举常量
▶ variable-list是变量

在这里插入图片描述

enum Day
{
	Mon,
	Tues,
	Web,
	Thur,
	Fri,
	Sat,
	Sun
}a,b;//创建全局变量
int main()
{
	//创建局部变量
	enum Day d = Mon;
	return 0;		 
}

🎗 上面说到枚举是常量,那么打印它们的值

#include<stdio.h>
enum Color 
{
	RED,
	GREEN,
	BLUE
};
int main()
{
	printf("%d\\n", RED);//0
	printf("%d\\n", GREEN);//1
	printf("%d\\n", BLUE);//2
	return 0;
}

❓❔既然枚举可能取值都有对应的常量值,那能否赋常量值呢

enum Color 
{
	RED,
	GREEN,
	BLUE
};
int main()
{
	enum Color c = 2;//?
	return 0;
}

📝 分析:

2是一个整型,而c是enum Color类型,所以是err。 发现在.c下执行代码并没有err,而.cpp中err
在这里插入图片描述
📝 小结:

这里只能说明C++对语法的检查更严格,虽然在C语言中并没有报错,但还是要避免这种写法


❓❔ 枚举常量值能否被修改,为什么常量能被修改

#include<stdio.h>
enum Color 
{
	RED = 5,
	GREEN,
	BLUE
};
int main()
{
	printf("%d\\n", RED);//5
	printf("%d\\n", GREEN);//6
	printf("%d\\n", BLUE);//7
	return 0;
}

📝 分析:

枚举常量值是可以修改的,且会按照最后修改的值往后递增。在定义枚举时是赋初值(默认是从0开始),当然出了枚举外部去修改时是err

💦 枚举的优点

❓❔ 我们可以使用#define定义常量,为什么还要使用枚举

#define RED 5
#define GREEN 8
#define BLUE 9
//==========================================
enum Color
{
	RED = 5,
	GREEN = 8,
	BLUE //9
};

注意:
▶ 增加代码的可读性和可维护性

▶ 和#define定义的标识符比较枚举有类型检查,更加严谨

▶ 防止命名污染(封装)

▶ 便于调试(而#define定义的常量在调试时不能看到标识符)

▶ 使用方便,一次可定义多个常量

对比
在这里插入图片描述
📝 小结:

▶ 代码1:可读性非常差(需要不断的向上翻阅代码来匹配功能)

▶ 代码2:可读性得到了改善(使用枚举)

💦 枚举的使用

enum Color 
{
	RED = 1
	GREEN = 2,
	BLUE = 4
};
enum Color clr = GREEN;//只能拿枚举常量给枚举变量赋值,才不会出现类型的差异

三、联合体

💦什么是联合体

🔑 联合也是一种特殊的自定义类型,这种类型定义的变量也包含一系列成员,特征是这些成员公用同一块空间 (所以联合体也叫共用体)

💦 联合类型的声明和定义

------------------------联合的声明样式------------------------
union tag
{
  member1;
  member2;
}variable-list;
------------------------解释------------------------
▶ union是联合体关键字
▶ tag是联合标签名,是自定义的
▶ union tag就是联合类型
▶ {}里面放的是成员列表
▶ variable-list是变量

在这里插入图片描述

#include<stdio.h>
union Un
{
	char c;
	int i;
};
int main()
{
	//定义联合体变量
	union Un n;
	printf("%d\\n", sizeof(n));//4
	return 0;
}

💨 结果:
在这里插入图片描述

❓❔ 为什么是4呢,接下来我们就来探讨一下联合体的特点

💦 联合的特点

#include<stdio.h>
union Un
{
	char c;
	int i;
};
int main()
{
	union Un n;
	printf("%p\\n", &n);
	printf("%p\\n", &n.c);
	printf("%p\\n", &n.i);
	return 0;
}

以上是关于桃猿三结义:结构枚举联合的主要内容,如果未能解决你的问题,请参考以下文章

详解C语言结构体枚举联合体

详解C语言结构体枚举联合体

C89:论结构体/枚举体/联合体的使用

自定义结构类型:结构体枚举联合

C语言结构体及其内存对齐枚举联合的介绍

自定义类型详解(结构体+位段+枚举+联合)