自定义类型详解

                                                                    你好

目录

结构体:

定义:

结构体的声明:

结构体的自引用:

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

结构体的内存对齐:

 结构体内存对齐的原因:

位段

定义:

位段的内存分配:

位段的跨平台问题:

 位段的应用

枚举

定义

声明

枚举的使用

枚举的优点 


结构体:

定义:

结构体(struct)是由一系列具有相同类型或不同类型的数据构成的数据集合。简单的来讲,我们知道整型要用int,浮点型要用float,字符要用char,现在我创造一个结构体使其既能存放整型,又能存放浮点型以及其他类型。

结构体的声明:

struct node
{
	int a;
	float b;
	int* arr[];
};

上图就是一个结构体的声明,我们可以看到,结构体里可以有不同的数据类型。

struct
{
 int a;
 char b;
 float c;
}x;
struct
{
 int a;
 char b;
 float c;
}a[20], *p;

上图是结构体的匿名声明,struct后面没有名称,在结构体末尾跟着变量,这样也可以声明一个结构体。但是,你只能在结构体的后面声明,不能离开结构体再声明。

再次看到代码,现在有一个问题:*p=&x,请问这样的写法合法吗?

不合法!有人会问,x是一个结构体类型的变量,那么他的地址就应该用结构体指针来存放,而*P刚好是结构体指针,那为什么不行呢?确实x是一个结构体类型的变量,而*P也是结构体指针,但是结构体是有身份区别的,还记得我们说过结构体后面要有名称吗,这个名字就是他的身份。名字不同,对应的身份就不同,结构体类型就不同。由于这里使用的是匿名结构体,所以我们不知道他们的身份是否一样,因此系统就会把这两个结构体当成不一样类型的结构体,那么就不能赋值。

结构体的自引用:

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

​//代码1
struct Node
{
 int data;
 struct Node next;
};

//代码2
struct Node
{
 int data;
 struct Node* next;
};

​

代码1的方式是否可以呢?

代码2的方式是否可以呢?

代码1:不行!代码的本意是模拟实现链表,想记录数据,并通过结构体的next找到下一个数据,但是因为这里存放的不是地址,所以我们找不到下一个数据的具体的值。并且这种写法还有一个致命的问题。请你算出sizeof(struct node)的大小。

你会发现完全就算不了,struct node是由一个int 4个字节和next组成的,一个next又是由一个int 和一个next组成的,而一个next又是......由此循环往复,无限套娃。

反观代码2,我用指针的形式存储,就能找到下一个数据,而且我也能够计算sizeof(struct node)的大小。

//代码3
typedef struct
{
 int data;
 Node* next;
}Node;

现在来看到代码3,我们用typedef 来重命名结构体为node,这样写代码是否可以呢?

不行哦,可以看到我在结构体内部也使用了node这个结构体,那么在逻辑上就会有我还没有创造出来,你就已经使用了这样的先后顺序混乱的错误。

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


truct Point
{
 int x;
 int y;
}p1; //声明类型的同时定义变量p1
struct Point p2; //定义结构体变量p2
//初始化:定义变量的同时赋初值。
struct Point p3 = {x, y};
struct Stu        //类型声明
{
 char name[15];//名字
 int age;      //年龄
};
struct Stu s = {"zhangsan", 20};//初始化
struct Node
{
 int data;
 struct Point p;
 struct Node* next; 
}n1 = {10, {4,5}, NULL}; //结构体嵌套初始化
struct Node n2 = {20, {5, 6}, NULL};//结构体嵌套初始化

​

到这里我们差不多把结构体给学完了!下面我会补充一个热门的结构体问题。

结构体的内存对齐:

直接上代码

typedef struct 
{
	int a;
	char b;
	char c;
	
}node1;
typedef struct
{
	char a;
	int b;
	char c;
}node2;
int main()
{
	printf("%d\n",sizeof(node1));
	printf("%d\n",sizeof(node2));
}

请输出结果:

 很多人看到这个结果会大吃一惊,为什么会是8和12捏,我感觉都应该是6啊?

这就是我们要学习的结构体内存对齐

那么什么叫做结构体内存对齐呢?请看结构体内存对齐的规则:

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

2. 其他成员变量要对齐到某个数字(对齐数)的整数倍的地址处。 对齐数 = 编译器默认的一个对齐数 与 该成员大小的较小值。 VS中默认的值为8

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

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

那么根据以上规则我们来做几道联系吧

struct S3
{
 double d;
 char c;
 int i;
};
printf("%d\n", sizeof(struct S3));

struct S4
{
 char c1;
 struct S3 s3;
 double d;
};
printf("%d\n", sizeof(struct S4));

 结构体内存对齐的原因:

我们也许会觉得好端端的的存放内存他不香吗,为什么要浪费这么多的空间呢?其实这与计算机内存读取有关,这里引用比特鹏哥的观点:

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

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

总体来说: 结构体的内存对齐是拿空间来换取时间的做法。

这个思想在编程中很常见,浪费空间来提升效率有时是一个不错的选择。

那么,当知道这些时,我们可以通过设计代码,使其既能减少内存又能提高效率。就像上文的struct1与2的例子明明都是一样的数据,但是struct 1的内存占用空间就比2要少。通用的方法是尽量把占用空间小的数据集中放在一起。

位段

定义:

位段的声明和结构是类似的,有两个不同: 1.位段的成员必须是 int、unsigned int 或signed int 。 2.位段的成员名后边有一个冒号和一个数字

struct A
{
 int _a:2;
 int _b:5;
 int _c:10;
 int _d:30;
};

 注意:位段每一个成员冒号后面的数字,代表该成员在内存中占用的二进制位大小。就如这里的成员a占用2个bit位,成员d占用30个bit位。而且要记住冒号后面数字的大小是不能超过前面成员类型大小的。

由此可知,位段的使用是为了节约空间大小。

可以发现和结构体相比位段还是能节约不少空间的,对于结构体来说,4个int 需要16个字节,这里只需要8个就能解决问题。 、

位段的内存分配:

当我们知道了位段只需要8个字节时,我们就会很好奇他的内存是怎样分配的。

struct A
{
	char a : 3;
	char b : 4;
	char c : 5;
	char d : 4;
};


int main()
{
	struct A s = { 0 };
	s.a = 10;
	s.b = 12;
	s.c = 3;
	s.d = 4;
}

以上述代码为例,位段首先估算一下开辟1个字节还是4个字节,显然我们这里先开辟一个字节就足以维持,然后a占3个比特,b占4个比特,到c 的时候已经不够用了,我们需要再开辟空间,满足你的需求,我们又开辟了1个字节,可是这时我们遇到了一个麻烦

第一个开辟的字节还没有用完,还有1个比特,现在有又开辟了一个字节。请问,我该用完这剩下的1个比特后,再用新开的字节,还是直接用新开的,旧的就不管了?

如果按照第一种情况,存放abcd我们只需要2个字节,如果按照第二种情况,我们就需要3个字节。此时有人会想,你不是说位段的存在是为了节约空间的吗,那么我们就只用2个字节好了,可事实真的是如此吗?下面以VS2022为例

 答案清晰可见,是3!

为了方便我们进一步看到内存空间的使用,我们给位段附上值,进行调试。

 上面是一个字节,从左到右是从低地址到高地址,我们来探究,a中的值是怎么存放的。

a:10,1010

b:12,1100

c:3,0011

d:4,0100

a只有3个字节,所以a只能存010,b 可以存1100,c可以存00011,d可以存0100,倘若是先放高地址再放低地址,那么应该是下图

 

 结果是0x62,0X03,0X04,,所以最终内存块存放的是62 03 04

我们来看看结果

结果正确!62 03 04 

整理思路,我们可以拿到下面这幅图

 这样我们就搞懂了位段的内存存储了!

位段的跨平台问题:

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

2. 位段中最大位的数目不能确定。(16位机器最大16,32位机器最大32,写成27,在16位机 器会出问题。

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

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

上述3和4我们刚才已经验证过了,在VS2022系统里面,是从右向左分配,没用完直接跳不需要把旧的空间用完。既然有新的为啥还要执迷于旧的呢,是不是铁子们

  

 位段的应用

位段在网络中应用的比较多,如:IP数据包格式

 

当我们在网络上给其他人发送消息,这个消息会封装成如上所示的一个数据包。如若不这么干,那这条信息跑到网络上能准确找到接收人吗?这是绝对不可能的。可以看出封装的部分的排列恰好都被设计成了int型宽度。大家想象一下,如若不使用位段而是用结构体来进行封装,我们在网络上传输的数据包将会变得巨大,使得网络状态变差。可如若在设计之初就用位段排列好,是不是就避免了未来所会发生的这种情况,可见位段的用法还是蛮重要的。
 

枚举

定义

在C/C++中枚举是一个被命名的整型常数的集合,在实际应用中我们经常把能够且便于一一列举的类型用枚举来表示。就比如:一周的星期、性别、月份……

声明

enum Color
{
	Red,
	Yellow,
	Blue
};

枚举类型的定义与结构体类似,不同在于结构体每个成员之间是用隔开的,而枚举成员之间却是用分隔。

其中enum为枚举类型的关键字,enum Color为枚举的类型,而{}中的内容为枚举类型可能的取值,也称为:枚举常量。这些可能的取值都是有值的,在未初始化的情况下,默认从0开始依次递增1。当然我们可以在其定义的时候初始化一些值,而那些未初始化的部分会从最后一个初始化数开始向后依次递增1。如下所示:

 

枚举的使用

enum color2
{
	red = 100,
	green,
	orange,
};
int main()
{
	enum color2 gay = red;
	//enum color2 purpoe = 100;
	return 0;
}

 注意:在给枚举变量赋值的时候,只能用枚举常量(也就是这里的Red、Yellow、Blue),不能使用这些常量所对应的值(也就是0、1、2),否则会由于类型的差异而导致编译不过的情况。

枚举的优点

我们可以使用 #define 定义常量,为什么非要使用枚举? 枚举的优点:

1. 增加代码的可读性和可维护性//譬如一些数字我不知道代表的是什么意思,但是用单词代替他们我就知道他们的含义。

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

3. 防止了命名污染(封装)

4. 便于调试

5. 使用方便,一次可以定义多个常量 enum Day//星期 { Mon, Tues, Wed, Thur, Fri, Sat, Sun }; enum Sex//性别 { MALE, FEMALE, SECRET }; enum Color//颜色 { RED, GREEN, BLUE }; enum Color//颜色 { RED=1, GREEN=2, BLUE=4 }; 

结语

好了以上就是本次博客的全部内容,大家要想掌握的话一定要多敲代码哦!

都看到这了,不给个赞吗?

 

这份博客👍如果对你有帮助,给博主一个免费的点赞以示鼓励欢迎各位🔎点赞👍评论收藏⭐️,谢谢!!!
如果有什么疑问或不同的见解,欢迎评论区留言欧👀。

好了我是Happysky,编程之路你我一起探索,让我们下期JAN。

  • 1
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 1
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值