你好
目录
结构体:
定义:
结构体(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。