文章目录
✨✨✨学习的道路很枯燥,希望我们能并肩走下来!
编程真是一件很奇妙的东西。你只是浅尝辄止,那么只会觉得枯燥乏味,像对待任务似的应付它。但你如果深入探索,就会发现其中的奇妙,了解许多所不知道的原理。知识的力量让你沉醉,甘愿深陷其中并发现宝藏。
前言
我们之前都简单的了解过结构体,但并没更进一步的了解,本篇通过讲解结构体,枚举,联合体进一步加强对自定义类型的使用与理解,如有错误,请在评论区指正,让我们一起交流,共同进步!
1.结构体
结构是一些值的集合,这些值称为成员变量。结构的每个成员可以是不同类型的变量。
1.1结构体的声明
1.1.1结构体基本结构
=》结构体关键字struct ;
=》结构体标签名 tag =》根据自己的实际情况为自定义类型命名
=》结构体成员列表 member- list =>可以是1个成员或多个成员
(成员可以是不同的类型,char,double,结构体等等)
=》结构体变量名 variable - list (不一定要声明变量,可以没有): 可以在定义结构体直接声明,也可以在主函数中命名; 这里在定义的时候直接声明的变量,是全局变量。我们一般推荐在主函数中通过类型struct tag声明变量。
注意:分号不可以丢!!!
struct tag
{
member-list;
}variable-list ; //这里的分号不能丢
1.1.2自定义结构体实现
假如我们要定义一本书的类型:其中要包含书名,作者,价格,编号
我们C语言所学的基本类型肯定就满足不了了,这时候我们通过自定义结构体实现。
自定义的结构体类型相当于int ,double ,float等类型
struct book=>就是书的类型
代码如下:
#include<stdio.h>
struct book
{
char book_name[20];
char author[20];
int price;
char id[15];
}sb3,sb4; //=》全局变量
int main()
{
struct book sb1;
struct book sb2;//局部变量
return 0;
}
1.1.3结构体的不完全声明(匿名结构体)
使用匿名结构体必须在声明的时候,就声明结构体变量,而且匿名结构体只能使用一次。
在没有标签的结构体,不能在主函数中通过类型声明变量。
例如(错误演示):struct sb1; //error
#include<stdio.h>
struct
{
char book_name[20];
char author[20];
int price;
char id[15];
}sb1,sb2; //=》全局变量
int main()
{
return 0;
}
省略掉标签名后,下面代码合理吗?我们想一下?
#include<stdio.h>
struct
{
char book_name[20];
char author[20];
int price;
char id[15];
}sb1;
struct
{
char book_name[20];
char author[20];
int price;
char id[15];
}* p;
int main()
{
p = &sb1;
return 0;
}
尽管上述代码的成员变量一样,但上述代码是非法的,编译器会把他们当成两个完全不同的类型并发生警告。所以我们需要注意!!!
1.2结构体的自引用
结构体的自引用,我们在数据结构中常见,线性数据结构(顺序表,链表),树形数据结构 (二叉树)。
我们怎么把链表连接起来呢,我们就用了自引用,我们自己创建的结构体(结点)包含存储的数据 (data),指向下一个结点的指针。
为什么要用指针呢?
解释如下:
//我们的初步想法:结构体里面存储一个结构体,意思想要找到下一个结构体
//下面无法计算出sizeof(struct Node)
//错误示范,这样的书写没有办法算出结构体多大,并且会无限套娃使编译器出现错误
struct Node
{
int data;
struct Node n;
};
//正确自引用 通过指针找到下一个结点
struct Node
{
int data;
struct Node* nest;
};
问题又来了,struct Node 这样的类型太长了,我们能简化一些吗?
所以我们可以使用 typedef 重新对结构体类型命名。
typedef struct Node
{
int data;
struct Node* nest;
}Node; =》结构体名称Node
为了偷懒有人这样写
typedef struct Node
{
int data;
Node* nest;
}Node; =》结构体名称Node
这样写是错误的,Node结构体名称是在重命名完后才有Node我们是不能提前使用的Node的
1.3结构体变量的定义和初始化
两种声明:定义时顺带声明,或者在主函数中使用结构体类型声明
初始化:定义时顺带初始化,或者在主函数中使用结构体类型初始化
struct bookauthor
{
char name[20];
int age;
};
struct book
{
char book_name[20];
char author[20];
int price;
char id[15];
//struct bookauthor s; //结构体中也可以嵌套结构体,但初始化时候需要 {},初始化内嵌结构体
}sb3 = {"C语言","作者",43,"HI02110"},sb4;//声明,初始化
struct book sb5;//也可以这样声明
int main()
{
struct book sb1 = { "《c++》","zhangsan",88,"PG1001" };//声明
printf("%s %s %d %s\n", sb3.book_name,
sb3.author, sb3.price, sb3.id);
return 0;
}
有顺序和无顺序结构体初始化
struct S
{
char c;
int a;
float f;
};
int main()
{
//有顺序
struct S s = { 'w', 10, 3.14f };
printf("%c %d %f\n", s.c, s.a, s.f);
//无顺序 ,乱序初始化
struct S s2 = { .f = 3.14f , .c = 'w', .a = 10 };
printf("%c %d %f\n", s2.c, s2.a, s2.f);
return 0;
}
1.4 结构体内存对齐
知道了结构体应用,那我们更进一步思考一下:如何计算结构体的大小?
我们来看以下例子:
struct S1
{
char c1;
int i;
char c2;
};
struct S2
{
char c1;
char c2;
int i;
};
int main()
{
printf("%d\n", sizeof(struct S1));
printf("%d\n", sizeof(struct S2));
return 0;
}
没有接触过的小伙伴可能会认为结构体中的类型占几个字节,结构体就多大,可能会算出1+4+1=6,1+1+4=6这两个结果。
实际答案是:12,8
这涉及了我们的结构体内存对齐
1.4.1offsetof
这里使用offsetof 宏:用来计算结构体成员相对于起始位置的偏移量
使用offsetof:头文件#include<stddef.h>
两个参数:结构体,结构体成员
c1在0号位置;i在4下标位置;c2在8下标位置,内存图如下:
红色的空间分配给S1(S2)空间但没有使用
1.4.2结构体对齐规则
结构体的对齐规则:
1. 结构体的第一个成员直接对齐到相对于结构体变量起始为0的地址处。
2. 从第二个成员变量开始,要对齐到某个(对齐数)的整数倍的地址处。 假如0,1已经偏移完,要从2位置开始对齐,此时的成员类型int对齐数是4,我们必须找到下标为4的整倍数处开始偏移,必须跳过2,3从4开始计算。
3. 对齐数 = 编译器默认的一个对齐数 与 该成员大小的较小值。
VS中默认的值为8;Linux环境下默认不设对齐数(为成员自身大小)
4. 结构体总大小为最大对齐数(每个成员变量都有一个对齐数)的整数倍。每个成员都有一个对齐数,其中最大的就是最大对齐数。
5. 如果嵌套了结构体的情况,嵌套的结构体对齐到自己的最大对齐数的整数倍处,结构体的整体大小就是所有最大对齐数(含嵌套结构体的对齐数)的整数倍。
计算对齐数:
对于嵌套结构体:嵌套的结构体对齐到自己的最大对齐数的整数倍处
例如嵌套的s3结构体需要从8的倍数开始对齐
为什么要内存对齐?
- 1 .平台原因(移植原因):
不是所有的硬件平台都能访问任意地址上的任意数据的;某些硬件平台只能在某些地址处取某些特定类型的数据,否则抛出硬件异常。 - 2.性能原因:
数据结构(尤其是栈)应该尽可能地在自然边界上对齐。
原因:为了访问未对齐的内存,处理器需要作两次内存访问;而对齐的内存访问仅需要一次访问。
struct S1
{
char c;
int i;
};
//如果不对齐,存储一个字节c,后面会紧跟 i 的四个字节,
//当以4个字节读取时,会读取到 i 的前3个字节,
//再次读取c时,会重复读取前三个字节,而内存对齐就避免了这样的情况
总结:结构体内存对齐是拿空间来换取时间的做法
所以为了设计结构体节省空间:让占用空间小的成员尽量集中在一起。例如:类似char,char集中在一起,在放其他类型
1.4.3修改默认对齐数
我们一般设置默认对齐数都是2的n次方
//括号里面放的就是默认对齐数
#pragma pack(4)
struct S1
{
double d;
char c;
int i;
};
//恢复默认对齐数
#pragma pack()
int main()
{
printf("%d\n", sizeof(struct S1));
return 0;
}
1.5 结构体传参
结构体传参使用地址传参
代码来分析使用地址传参:
struct S
{
int data[1000];
int num;
};
void print(struct S s)
{
printf("%s %s %s %s\n", s.data[0], s.data[1], s.data[2], s.num);
}
void print2(struct S* ps)
{
printf("%d %d %d %d\n", ps->data[0], ps->data[1], ps->data[2], ps->num);
}
int main()
{
struct S ss = { {1,2,3,4,5},100 };
print(ss);//值传递,浪费空间,形参是临时拷贝,拷贝空间过大就会浪费空间
print2(&ss);//地址传递,只是4/8字节的地址,节省空间
return 0;
}
使用地址传参原因:
1.函数传参的时候,参数是需要压栈,会有时间和空间上的系统开销。
2.如果传递一个结构体对象的时候,结构体过大,参数压栈的的系统开销比较大,所以会导致性能的
下降。
1.6 结构体实现位段
位段是基于结构体出现的,说了结构体我们再来看看位段。
1.6.1认识位段
位段的声明类似结构体,但有两个不同:
1. 位段的成员必须是 int,unsigned int 或者signed int
2. 位段的成员名后边有一个冒号和一个数字
A就是一个位段类型:位就是二进制位(比特位)
struct A
{
int _a : 2;
int _b : 5;
int _c : 10;
int _d : 30;
};
struct S
{
int a;
int b;
int c;
int d;
};
int main()
{
printf("%d\n", sizeof(struct S));
printf("%d\n", sizeof(struct A));
return 0;
}
知道了位段,那它的大小又是多少?结果和你想的一样吗?
字段的使用:
结构体A中是字段,a是int类型开始先开辟4个字节(32个bit)供_a使用2个bit,剩余30个bit,_b使用5个bit,剩余25个bit,_c使用10个bit,剩余15个bit,_d要使用30个bit,剩余bit不够,就需要重新开辟,因为是int类型所以会再开辟4个字节空间(32个bit).
那么问题来了,有了新开辟的空间它还会继续使用原有剩下的bit空间吗?
我们有两种想法:
1.接着使用剩余的bit,不够了再使用新的bit空间
2.直接丢弃剩余的空间,使用新的bit空间
这间接表示了位段的不确定性。
我们来看一下位段的内存分配,进一步了解一下!
1.6.2位段的内存分配
- 位段的成员是int, unsigned int, signed int 或者是char 类型
- 位段的空间上是按照需要以4个字节或1个字节的方式开辟的
- 位段涉及很多不确定因素,位段是不跨平台的,注重可移植的程序应避免使用位段
例子:
struct S
{
char a:3;
char b:4;
char c:5;
char d:4;
};
struct S s = {0};
s.a = 10;
s.b = 12;
s.c = 3;
s.d = 4;
我们通过这个例子理解一下字段:
我们首先要明白char - 1个字节 - 8个bit位
我们存放数据是从左向右,还是从右向左这是不确定的,这里我们不会从中间开始放置,这是不合理的,数据一般从低地址往高地址存放或者从高地址往低地址存放。
这里我们先假设规则①从右向左存储数据,②如果剩余空间不够存放下一个数据,我们就丢掉,根据类型重新开辟一个空间继续存放数据。
我们先创建结构体S,猜测它占多大的内存
白色的空间丢掉根据制定的规则
根据下图我们可以知道我们的思路大概没错,但是其中的细节又是什么,我们再来研究一下!
我们通过图解理解位段的存储
1.6.3位段的跨平台问题
1. int 位段被当成有符号数还是无符号数是不确定的。
2. 位段中最大位的数目不能确定。(16位机器最大16,32位机器最大32,写成27,在16位机器会出问题。
3. 位段中的成员在内存中从左向右分配,还是从右向左分配标准尚未定义。
4. 当一个结构包含两个位段,第二个位段成员比较大,无法容纳于第一个位段剩余的位时,是舍弃剩余的位还是利用,这是不确定的。
位段存在跨平台问题,使用时注意
2. 枚举
2.1枚举类型的定义
枚举 – 就是一一列举
可能的取值 -》列举
我们生活中的一周的星期是有限的7天,性别只有男女,月份只有12个月。
枚举关键字:enum
枚举标签:Day
括号里面的:可列举的值用逗号隔开,最后一个不用加逗号
enum Day
{
Mon,
Tues,
Wed,
Thur,
Fri,
Sat,
Sun
};
//创建一个颜色的类型
enum Color
{
RED,
GREEN,
BLUE
};
int main()
{
enum Color color = BLUE;
return 0;
}
颜色类型的枚举变量color它的取值只能是枚举里面包含的值(BLUE,GREEN,BLUE)它是固定的。
枚举的可能取值,每一个可能的取值是常量,所以大括号中的值为枚举常量。
这些可能取值都是有值的,默认从0开始,一次递增1,当然在定义的时候也可以赋初值,赋值完后的枚举常量依次+1
2.2 枚举的优点
我们可以使用#define 定义常量,但是#define没有类型,不够严谨,这就体现出枚举的优点
enum Color
{
RED = 5,
GREEN = 7,
BLUE = 10
};
#define RED 5
#define GREEN 7
#define BLUE 10
枚举优点:
1. 增加代码的可读性和可维护性
2. 和#define定义的标识符比较枚举有类型检查,更加严谨。
3. 防止了命名污染(封装)
4. 便于调试
5. 使用方便,一次可以定义多个常量
3. 联合
3.1 联合类型的定义
联合是一种特殊的自定义类型
我们要注意类似结构体但他们是不同的。这种类型定义的变量也包含一系列的成员,特征是这些成员公用同一块空间(所以联合也叫共用体)。
union Un
{
char c;
int i;
double d;
};
int main()
{
union Un un;//联合体定义
printf("%d\n", sizeof(union Un));//8
printf("%d\n", sizeof(un));//8
return 0;
}
3.2 联合的特点
联合的成员是共用同一块内存空间的,这样一个联合变量的大小,至少是最大成员的大小(因为联合至少得有能力保存最大的那个成员)
union Un
{
char c;
int i;
double d;
};
int main()
{
union Un un;
printf("%p\n", &un);
printf("%p\n", &(un.c));
printf("%p\n", &(un.i));
printf("%p\n", &(un.d));
return 0;
}
通过结果我们可以知道他们的起始地址都一样。
他们存放在空间中,如下图,为了方便看,不使用一个图表示,我们分成3个图观看,但其实是一块空间,使用时会覆盖。
使用联合体其中一个成员,不同时使用多个联合体成员,就可以使用联合体。
使用联合体判断大小端问题?
联合体:利用联合体特点共用一块内存
u.i =1; 将1存放在int4个字节中,小端u.c取的是01,大端取的是00
u.c只取第一个字节里的内容
int check_sys()
{
union
{
char c;
int i;
}u;
u.i = 1;
return u.c;
}
int main()
{
int ret = check_sys();
if (ret == 1)
{
printf("小端\n");
}
else
{
printf("大端\n");
}
return 0;
}
回忆函数判断大小端:
int check_sys()
{
int num = 1;//取四个字节中第一个字节
char* p = (char*)#// int*=》转化char*类型
if (*p == 1)
{
return 1;
}
else
{
return 0;
}
//简化可直接返回 return *(char*)#//是0或1直接返回
}
int main()
{
int ret = check_sys();
if (ret == 1)
{
printf("小端\n");
}
else
{
printf("大端\n");
}
return 0;
}
3.3 联合大小的计算
1. 联合的大小至少是最大成员的大小。
2. 当最大成员大小不是最大对齐数的整数倍的时候,就要对齐到最大对齐数的整数倍。
union Un
{
char arr[5];
int i;
};
int main()
{
printf("%d\n", sizeof(union Un));
return 0;
}
注释:开辟的4个字节不够字符数组共用,所以需要开辟新空间共用,因为最大对齐数是4,开辟的空间必须是4的整倍数,所以开辟8个字节就够用了。(5,4是所占字节数)
我们来练习一下吧!
union Un
{
short a[6];// 12; 2,8=>2
int i; // 4 ; 4,8=>4
};
int main()
{
printf("%d\n", sizeof(union Un));
return 0;
}
总结
✨✨✨各位读友,本篇分享到内容是否更好的让你理解了自定义类型,对结构体有了新的认识,如果对你有帮助给个👍赞鼓励一下吧!!
🎉🎉🎉一遇挫折就灰心丧气的人,永远是个失败者。而一向努力奋斗,坚韧不拔的人会走向成功。
感谢每一位一起走到这的伙伴,我们可以一起交流进步!!!一起加油吧!!!