前言
作者:小蜗牛向前冲
名言:我可以接收失败,但我不能接收放弃
如果觉的博主的文章还不错的话,还请 点赞,收藏,关注👀支持博主。如果发现有问题的地方欢迎❀大家在评论区指正。
在C语言中常见的数据类型:
整形:int short long long long char(字符类型)
浮点型 flaot double
这些类型是C语言都帮我们定义好的,下面我们将继续学习自定义类型。
自定义类型
简单的来说就是不由系统定义,而是由程序设计者自己定义的类型,在本篇博主中,我们重点分享结构体,位段,枚举,联合相关知识。
结构体
理解
结构是一些值的集合,这些值称为成员变量。结构的每个成员可以是不同类型的变量。
结构体的声明
是借助关键字struct来进行的。
struct关键字是用来定义一个新的类型,这个新类型里面可以包含各种其他类型,称为结构体。结构体 (struct)是一种自定义的数据类型,就是把一组需要在一起使用的数据元素组合成一个新的类型。
1形式
struct tag
{
member-list;
}
variable-list;
tag:是结构体的标签
member-list:是结构体的成员
variable-list:这是给结构体取的变量名
注意:
定义结构体时成员是不用初始化的。
结构体的变量名可以在定义是就声明,也可以在需要的时候定义。
代码演示:
#define _CRT_SECURE_NO_WARNINGS #include<stdio.h> struct studen { char* name;//姓名 int num;//学号 int age;//年龄 };
这样我们就简单的声明来一个结构体,结构体的标签为studen,我们可以看到这里并没有给结构体取变量名。
2 特殊声明(匿名结构体)
就是指在声明结构体的时候,不写结构体的标签。因为没有结构体的标签,所以只能用一次。
#include<stdio.h> //匿名结构体类型 struct { int a; float b; char c; }s1; struct { int a; float b; char c; }a[20],*p; int main() { p = &a;//? }
我们可以看的到什么的代码,这样是可以行的吗?我们编译起来
发现出现一个警告,为什么呢?这二个结构体的成员不都是一样的吗?他们不相同吗?
在上面我也说过匿名结构体只能用一次,所以说匿名结构体即使成员相同,但由于是匿名的,只能使用一次,二者结构体是不相同的,我们应该慎重使用匿名结构体(可以在只使用一次的场景使用)。
3结构体的自引用
在结构中包含一个类型为该结构本身的成员是否可以呢?
struct data { int a; struct data* next; };
其实是可以的,但我们不能直接存储结构体本身,而要存放结构体的地址。
为什么呢?
其实如果存放结构体本身,因为要不断自引用结构体,这使得结构体的大小就不确定了,而且这个结构体的大小将会非常大,无法存储。所有我们存放结构体的地址就这样就使得结构体的大小确定(指针的大小是确定的),我们还可以通过结构体的地址找到结构体的成员,这样链表就得以实现。
4 结构体变量的定义和初始化
结构体变量的初始化我把他归类为3类:
#include<stdio.h> //类型1 struct data { int a; int b; int c; }p1;//定义结构体的时候,定义变量名p1 //类型2 struct studen { struct data; const char* name; int age; }; //类型3 struct age { int year; int month; int day; }p3; void print(struct age* p3) { p3->year = 1949; p3->month = 10; p3->day = 1; printf("year = %d month = %d day = %d\n", p3->year, p3->month, p3->day); } int main() { struct data p1 = { 1,2,3 };//赋值 printf("%d %d %d\n", p1.a, p1.b, p1.c); struct studen p2 = { 4,5,6,"zhangshan",18 }; printf("%d %d %d\tname = %s age = %d\n", p2.a, p2.b, p2.c, p2.name, p2.age); //struct age p3 = { 1949,10,1 }; print(&p3); return 0; }
类型1
在struct data结构体定义的时候为结构体定义了名字p1,初始化直接用大括号初始就可以了。
类型2
在struct studen结构体定义的时候并没有为结构体取名字,在初始化的在取名为p2,要是可以的,值得说明的是在结构体在包含结构体初始化也是没说明区别的。
类型3
其实这个类型3和其他类型的区别主要是,他在初始化的时候是在函数中,我们为函数传递了结构体strcut age的地址,所有要初始化结构体要用" -> "。
我们知道结构体是怎么定义和初始化的,那么我们又是如何访问结构体中的成员的呢?
结构体变量是指针用:" -> "访问。
结构体变量非是指针用:" . "访问。
5 结构体内存对齐
上面我们提到结构体自引用时,传的是指针,而不是结构体本身是为防止结构体大小过大。那么结构体的大小又是如何计算的呢?
#include<stdio.h> //1 struct studen { const char* name; int age; }p1; //2 struct data { int a; double b; char c; }p2; //3 struct datas { char c; int a; double b; }p3; int main() { printf("%u %u %u\n", sizeof(p1), sizeof(p2), sizeof(p3)); }
下面这些结构体的大小是多少呢?
是把结构体成员的大小都加起来吗?
结构体1
1+4 =5?
结构体2和结构体3的成员相同
1+4+8=13?
是上面这么结果对面,下面我们打印常出来看看。
8 24 16 为什么啊,这就不得不提结构体大小的计算方法结构体内存对齐。
结构体内存对齐的规则:
1. 第一个成员在与结构体变量偏移量为0的地址处。
2. 其他成员变量要对齐到某个数字(对齐数)的整数倍的地址处。 对齐数 = 编译器默认的一个对齐数 与 该成员大小的较小值。 VS中默认的值为8
3. 结构体总大小为最大对齐数(每个成员变量都有一个对齐数)的整数倍。
4. 如果嵌套了结构体的情况,嵌套的结构体对齐到自己的最大对齐数的整数倍处,结构体的整体大小就是所有最大对齐数(含嵌套结构体的对齐数)的整数倍。
下面我们将画图来理解为什么结构体1,2,3的大小是8,24,16。
图1
图2
图3
不知道大家观察到没,p2和p3的成员是一样的就是定义的顺序不同,但分配的内存空间大小是不一样的,而且字节大小越小的成员先定义,那么分配到的空间就更少,浪费的内存空间也跟小。
总结
总体来说:
结构体的内存对齐是拿空间来换取时间的做法。
那在设计结构体的时候,我们既要满足对齐,又要节省空间,如何做到:
让占用空间小的成员尽量集中在一起。
为什么要存在内存对齐呢?
1. 平台原因(移植原因): 不是所有的硬件平台都能访问任意地址上的任意数据的;某些硬件平台只能在某些地址处取某些特 定类型的数据,否则抛出硬件异常。
2. 性能原因: 数据结构(尤其是栈)应该尽可能地在自然边界上对齐。 原因在于,为了访问未对齐的内存,处理器需要作两次内存访问;而对齐的内存访问仅需要一次访问。
上面我们提到Vs是有默认对齐数(8)的,由于对齐数影响结构体在内存在的分配,有时候我们为了控制分给结构体的内存需要更改默认对齐数,可以通过#pragma 这个预处理指令,这里我们再次使用,可以改变我们的默认对齐数。
#pragma pack(1)//设置默认对齐数为1 struct day { int a; int b; int c; }; #pragma pack();//恢复默认对齐为8
6结构体传参
struct s { int data[10]; int num; }p1; //传值 void print1(struct s p) { printf("%d\n", p.num); } //传地址 void print2(struct s* p) { printf("%d\n", p->num); } int main() { struct s p1 = {{1,2,3,4,5},10}; print1(p1);//传值 print2(&p1);//传地址 }
对于上面二种传参方式,我们选择那种呢?
肯定是print2
为什么呢?
原因:
函数传参的时候,参数是需要压栈,会有时间和空间上的系统开销。 如果传递一个结构体对象的时候,结构体过大,参数压栈的的系统开销比较大,所以会导致性能的下降。
结论:
结构体传参的时候,要传结构体的地址。
位段
位段又是啥子呢?下面我们先了解位段的声明
位段的声明和结构是类似的,有两个不同:
1.位段的成员必须是 int、unsigned int 或signed int或者char 。
2.位段的成员名后边有一个冒号和一个数字。
1 声明
struct data { char a : 2; int b : 2; long long c : 4; }; int main() { printf("%u\n", sizeof(struct data)); }
data就是一个位段,那么的占几个字节呢?
从中可以看出位段也是遵循内存对齐规则的。
2 位段的内存分配
1. 位段的成员可以是 int unsigned int signed int 或者是 char (属于整形家族)类型
2. 位段的空间上是按照需要以4个字节( int )或者1个字节( char )的方式来开辟的。
3. 位段涉及很多不确定因素,位段是不跨平台的,注重可移植的程序应该避免使用位段。
下面我们继续研究一下位段的内存分配。
struct s { char a : 2; char b : 4; char c : 5; }; int main() { struct s p1 = { 0 }; p1.a = 2; p1.b = 4; p1.c = 10; return 0; }
从图中我们可以看出,位段其实就是可以自己分配内存给自己使用。其中" : "后面的数字是分配的给内存几个bit位用来存储成员。(vs的分配方式)
3位段的跨平台问题
1. int 位段被当成有符号数还是无符号数是不确定的。
2. 位段中最大位的数目不能确定。(16位机器最大16,32位机器最大32,写成27,在16位机器会出问题。
3. 位段中的成员在内存中从左向右分配,还是从右向左分配标准尚未定义。
4. 当一个结构包含两个位段,第二个位段成员比较大,无法容纳于第一个位段剩余的位时,是 舍弃剩余的位还是利用,这是不确定的。
4 位段的应用
主要运用于数据传输中。
枚举
枚举顾名思义就是一一列举。
把可能的取值一一列举。
1枚举的定义
enum peo//个人信息 { name, sex, Stature, age };
以上定义的 enum peo是枚举类型。 {}中的内容是枚举类型的可能取值,也叫枚举常量 。 这些可能取值都是有值的,默认从0开始,一次递增1,当然在定义的时候也可以赋初值。
2枚举的优点
为什么使用枚举?
我们可以使用 #define 定义常量,为什么非要使用枚举? 枚举的优点:
1. 增加代码的可读性和可维护性
2. 和#define定义的标识符比较枚举有类型检查,更加严谨。
3. 防止了命名污染(封装)
4. 便于调试
5. 使用方便,一次可以定义多个常量
对于枚举很多人可能觉的没什么用,其实不然,枚举还是有许多优点的,我们在以后的编译时遇到一定要细细体会。
3 枚举的使用
enum peo//个人信息 { name, sex, Stature, age }; //使用 enum peo mam = name;//只能拿枚举常量给枚举变量赋值,才不会出现类型的差异。
枚举的使用场景还需大家在合适的场景使用。
联合(共用体)
1 联合类型的定义
联合也是一种特殊的自定义类型 这种类型定义的变量也包含一系列的成员,特征是这些成员公用同一块空间(所以联合也叫共用体)。
比如:
//联合体 //声明 union s { char a; int b; }; int main() { //联合体变量的定义 union s un; //计算联合体的大小 printf("%d\n", sizeof(un)); }
2 联合的特点
联合的成员是共用同一块内存空间的,这样一个联合变量的大小,至少是最大成员的大小(因为联 合至少得有能力保存最大的那个成员)。
我们思考一个问题?
&(un.a)和&(un.b)得到的地址是一样的吗,可以肯定是一样的,为什么怎么说呢?因为二者公用一个内存的话,二者指针指向的位置必须是一样的这样才能找到相应的空间。
3联合大小的计算
联合的大小至少是最大成员的大小。
当最大成员大小不是最大对齐数的整数倍的时候,就要对齐到最大对齐数的整数倍
下面看到思考题,来结束今天的分享。
union Un1 { char c[5]; int i; }; union Un2 { short c[7]; int i; }; //下面输出的结果是什么? int main() { printf("%d\n", sizeof(union Un1)); printf("%d\n", sizeof(union Un2)); return 0; }
这里就不过多解释了。