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

在这篇文章中,我将为大家带来自定义类型:结构体、位段、枚举、联合的详细知识。





内置类型和自定义类型

在C语言中,类型分为内置类型和自定义类型。

内置类型:char、short、int、long、long long、float、double、long double、bool类型。

自定义类型:结构体、位段、枚举、联合。



结构体

1.结构体的基础知识

结构体是一些值的集合,这些值被称为成员变量。结构体的每一个成员可以是不同类型的变量。



2.结构体的声明和结构体变量的定义

完全声明
假如我声明一个关于学生信息的结构体,包含名字、年龄、学号等信息。如下:

struct stu                      //注意这里struct后面加了名字
{
    char name[20];              //名字
	int age;                    //年龄
	char StudentName[20];       //学号
};                              //注意加分号

这个关于学生信息的结构体就被声明成功了,那么在声明之后,我应该怎么开辟一个结构体的变量呢?别急,先听我分析。

#include<stdio.h>
struct stu                      //注意这里struct后面加了名字
{
    char name[20];              //名字
	int age;                    //年龄
	char StudentName[20];       //学号
};                              //注意加分号
int main()
{
	struct stu s[3] = {{"zhangsan",20,"202212345678"},{"lisi",25,"2022123456789"},{"wangwei",29,"1234567891"}};
    return 0;
}

如上面代码,我创建了结构体数组,分别存放着zhangsan同学、lisi同学、wangwei同学的信息。

不完全声明

我依然通过举例子来说明结构体的不完全声明。如下:

struct               //注意struct后面没加名字
{
	int a;
	char b[20];
	float c;
};

我在上面已经写了两个结构体的声明了,第一个声明是完全声明,第二个是不完全声明,我们来观察一下两个结构体声明有什么不同,从而导致了这两种是不同类型的声明。当然,这里肯定不是成员变量的不同所导致的。

在第一个结构体的声明中,struct后面加了名字,而在第二个结构体声明中,struct后面并没有加名字。综上所述,在结构体声明中,struct后面没有加名字的,就叫做不完全声明。

不完全声明中定义变量的注意事项
在第一个定义结构体变量中,我是通过 struct + 名字 找到结构体类型,再进行定义变量和赋初值的。那么在第二个定义结构体变量,也就是不完全声明的,我该如何找到结构体类型,进行定义变量和赋初值呢?直接struct找到吗?我举个例子:

struct                              //不完全声明
{
	int a;
	char b[20];
	float c;
};
int main()
{
	struct s[] = {10,"20",1.0f};     //s是数组名字
    return 0;
}

我运行一下:
请添加图片描述
在vs1010的运行中,直接报出了应输入标识符和struct未定义符号的错误,也就是,不完全声明无法通过struct直接找到结构体类型进行定义变量。这一下,就有小伙伴疑惑了,结构体的不完全声明时该怎么定义变量和赋初值?

接下来,我就来讲讲结构体不完全声明中正确的定义变量的方法,我依然以例子的方式来进行讲解。

struct
{
	int a;
	char b[20];
	float c;
}s={20,"a",1.0f};         //定义加赋值
int main()
{  
    return 0;
}

我依然运行一下,代码成功跑起来。

综上所述,在不完全声明的结构体中,如果我们要进行定义变量和赋初值时,必须要在声明该结构体后面进行操作。

两个结构体成员相同的不完全声明结构体是否会是同一个结构体

struct
{
    char book_name[20];
	int price;
}s1;

struct
{
    char book_name[20];
	int price;
}*ps;

int main()
{
	ps = &s1;
    return 0;
}

在上面的代码中,我声明两个未完成声明的结构体,并且保证这两个结构体的成员变量是相同的。在第一个未完全声明的结构体中,我定义了s1的结构体变量,在第二个未完全声明的结构体中,我定义指针ps,然后将s1的地址赋值给指针ps,如果成功的话,表明这两个未完全声明的结构体是同一个。

请添加图片描述
在vs2010中,这种行为是错误的。所以,两个不完全声明的结构体虽然成员名字是一样的,但是依然是不同的变量类型



结构体的自引用

链表中,一个结点存放着数据和下一个结点的地址(方便找到下一个结点)实现了引用。

在结构体中,也是这个原理。

struct Node
{
    int data;
	struct Node* next;           //指向下一个结构体
};


typedef在结构体中的应用

typedef struct Node      //typedef起作用   
{
    int data;             
	char num[20];
}Node;                  //改名为Node

我们可以注意到,typedef改结构体的名字时,新更换的名字写在定义结构体的后面、冒号前的位置。

注意事项
typedef作用结构体时,如果结构体存在着自引用时,自引用的部分不能采用新的结构体名字,如

typedef struct Node
{
	int data;
	char num[20];
	struct Node* next;           //这里的struct Node不能改为Node
}Node;

在结构体的自引用中,依然采用旧的结构体名字进行定义,因为新的结构体名字,如上面的Node只有在整段代码实行完才生效。

用typedef改名后,难以识别类型,如Node s1 可能不知道是结构体还是枚举,还是联合。



结构体的内存对齐

现在我来探讨一下结构体的大小,提到了结构体的大小,必然要提到结构体的内存对齐,这是结构体最有趣的部分之一。

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

如果我们一开始并不了解结构体的内存对齐,那么在对上面代码的结果进行猜测时,往往会出现错误的结果。

我们可能认为,s1和s2都是两个char类型数据和一个int类型的数据,那么,它们的大小可能是6个字节,结果是这样吗?

运行结果:
请添加图片描述

由运行结果可以发现,结构体的大小并不是成员类型数据大小的叠加,那么,想要解释这一结果是从何而来的,便要讲解一下结构体的内存对齐。

我先来介绍一下结构体的内存对齐的规则

1.结构体的第一个成员直接对齐到相对结构体变量起始位置为0的偏移处。
2.从第二个成员开始,要对齐到某个对齐数的整数倍的偏移处。对齐数:结构体成员自身大小和默认对齐数的较小值。
vs的默认对齐数是8
linux环境默认不设对齐数(对齐数是结构体成员的自身大小)
3.结构体的总大小,必须是最大对齐数的整数倍,每个结构体成员都有一个对齐数,其中最大对齐数就是结构体成员中的所有对齐数的最大值。
4.如果嵌套了结构体的情况,嵌套的结构体对齐到自己最大对齐数的整数倍处。结构体的整体大小就是最大对齐数(含嵌套结构体的对齐数)的整数倍。

对于结构体的内存对齐规则,我已经介绍清楚了。接下来,我来举例子对这些规则进行讲解。

struct s1
{
    char c1;
	char c2;
	int i;
};

在前面我已经测试过这个s1结构体的大小,为8,那么我就用这个例子来讲解结构体的内存对齐规则吧。

在讲解之前,我先介绍一个新的概念,偏移量。偏移量就是字节相对起始位置称为偏移量。
请添加图片描述
如上面的内存图中,结构体假设从第二个方框开始存储数据(注意这个是假设),那么第二个方框相对于起始位置的偏移量就是0,第三个方框相对于起始位置的偏移量就是1,第四个方框相对起始位置的偏移量就是2,依次类推。

现在我开始介绍s1的内存大小为8的原因。按照结构体的内存对齐规则第一条,结构体的第一个成员也就是结构体s1中的c1要对齐到相对结构体变量起始位置为0的偏移处。
请添加图片描述
如上面的内存图,c1存储在偏移量为0的位置。

接下来,我来存储结构体s1的第二个成员c2。由结构体的内存对齐规则第二条可知,从第二个成员开始要对齐到对齐数的整数倍处,那么我要找出这个对齐数。对齐数是结构体成员自身大小和默认对齐数的较小值。

我现在使用的是vs的编辑器,那么由结构体的内存对齐规则可以得知,vs的默认对齐数是8,而这个结构体的第二个成员的类型为char,大小为1的字节。那么,在8和1挑选较小值,就是1,所以,这个结构体成员的对齐数是1。

偏移量为1时,正是这个结构体成员的对齐数的整数倍,那么c2直接存在偏移量为1处。
请添加图片描述
如上面的内存图,c2存储在偏移量为1的位置。

接下来,我来存储结构体s1的第三个成员,也就是i。我依然要求出这个结构体成员的对齐数,由于在vs的编辑器下运行,所以默认对齐数是8,而这个结构体成员的自身类型是int类型,大小是4个字节,现在挑选,默认对齐数和结构体成员的自身大小的较小值,毫无疑问这个较小值是4。

现在偏移量为0和偏移量为1已经存储完毕,开始从偏移为2的位置开始寻找4的整数倍进行存储这个结构体成员i,第一个寻找到的位置当然是偏移量为4的位置,那么,在这个位置开始存储结构体成员i。

请添加图片描述
如上面的内存图,i存储在偏移量为4的位置开始存储。(注意i是int类型,所以要存储4个字节)

那么,这时应该有人对于结构体成员c2和结构体成员i的中间这两个字节内存的存储存在疑问,可能这个疑问就是这两个字节空间存什么。我想说的是,这两个字节内存是不存储东西的,浪费掉了。本来,结构体的内存对齐就是以牺牲空间来换取效率的(这个后面会讲解)。

现在结构体s1的三个结构体成员已经存储完成,我们来数一下现在所占用的内存大小(单位是字节),注意的是这里中间的两个没有存储数据的字节也要算进去。
请添加图片描述
由结构体的内存对齐规则的第三条可知,结构体的总大小,必须是最大对齐数的整数倍,结构体s1中的三个结构体成员的对齐数分别是0、1、4,那么最大对齐数是4。结构体的总大小是8,是最大对齐数4的整数倍,所以满足了结构体的内存对齐规则的第三条,所以结构体s1的内存是8。

由内存图可以得知,结构体s1的三个结构体成员的偏移量分别是0、1、4。

接下来,我来引入一个新的函数来证明一些上面的猜想。这个函数就是offsetof函数,是用来计算结构体成员相对于起始位置的偏移量。

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

运行结果如下:
请添加图片描述
由程序运行结果得知的结构体s1的各个结构体成员的偏移量与前面我们求出的结构体s1的各个结构体成员偏移量是相同的,证明前面的猜想是正确的。

struct s2
{
	char c1;
	int i;
	char c2;
};

在前面的我已经求出结构体s2的大小了,是12。现在,我来利用结构体的内存对齐规则,对结构体s2进行存储,了解这个结构体s2的内存大小是12的原因和提高对结构体内存对齐规则的使用能力。

由结构体的内存对齐规则第一条,结构体的第一个成员直接对齐到相对结构体变量起始位置为0的偏移处,所以,结构体s1的结构体成员c1要对齐到偏移量为0的位置。
请添加图片描述
由结构体的内存对齐规则第二条,从第二个成员开始,要对齐到某个对齐数的整数倍的偏移处。对齐数:结构体成员自身大小和默认对齐数的较小值。所以,我要求出i的对齐数。我采用的是vs编辑器,默认对齐数是8,而i的类型是int类型,自身大小是4个字节,在默认对齐数和结构体成员自身大小选较小值,就是4,则结构体成员i的对齐数是4。

结构体成员i要对齐到对齐数的整数倍也就是4的整数倍的偏移处。我从偏移量为1开始寻找(偏移量为0已经存储完成),毫无疑问,结构体成员i要开始存储的位置是偏移量为4的位置。偏移量1、2、3的三个字节内存直接浪费掉。

请添加图片描述
如上面的内存图,结构体成员i从偏移量为4开始存储,因为结构体成员i是int类型,所以要存储4个字节。

接下来,我来存储结构体成员c2。
依然要求结构体成员c2的对齐数,我采用的是vs编辑器,默认对齐数是8,而结构体成员c2是char类型,自身大小是1个字节,那么默认对齐数和自身大小的较小值是1,所以结构体成员c2的对齐数是1。

那么,结构体成员c2要对齐到对齐数的整数倍也就是1的整数倍处。

上次存储结构体成员i时,已经存储到偏移量为7的位置,那么结构体成员c2要存储时,寻找对齐数就从偏移量为8的位置开始寻找,毫无疑问,偏移量为8确实是对齐数1的整数倍。
请添加图片描述
如上面的内存图,结构体成员c2存储在偏移量为8的位置。

现在,我来计算结构体成员s2的大小。
请添加图片描述
如图所示,结构体s2的大小是9个字节。这下,就有人疑问了,前面采用sizeof计算出来的结构体s2的大小是12个字节,那么哪一个是对的?不急,我们先看看结构体内存对齐规则的第三条,结构体的总大小,必须是最大对齐数的整数倍,每个结构体成员都有一个对齐数,其中最大对齐数就是结构体成员中的所有对齐数的最大值。

结构体s2的结构体成员c1、i、c2的对齐数分别是0、4、1,那么,最大对齐数是4,而上面求的结构体大小9是最大对齐数4的整数倍吗?答案是否定的。那么,就要继续往后面寻找偏移量,直到结构体大小是最大对齐数4的整数倍。

当偏移量为11时,结构体s2的大小是12,是最大对齐数的4的整数倍,所以,结构体s2的大小是12。
请添加图片描述
其中,偏移量为1、2、3、9、10、11的空间都被浪费了。

由内存图可以得知,结构体s2的三个结构体成员的偏移量分别是0、4、8。

现在,我依然使用offsetof函数来验证结构体s2的各个结构体成员的偏移量是不是正确的。

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

运行结果如下:
请添加图片描述
由程序运行结果得知的结构体s2的各个结构体成员的偏移量与前面我们求出的结构体s2的各个结构体成员偏移量是相同的,证明前面的猜想是正确的。

接下来,我来讲解嵌套结构体的情况。

#include<stdio.h>
struct s1
{
	char c1;
	char c2;
	int i;
};
struct s3
{
    char d1;
	struct s1 t;
	int r;
};
int main()
{
    printf("%d\n",sizeof(struct s3));
	return 0;
}

运行结果如下:
请添加图片描述
在前面我已经讲解了结构体s1的大小,为8。由结构体的内存对齐规则第四条可以得知,如果嵌套了结构体的情况,嵌套的结构体对齐到自己最大对齐数的整数倍处。结构体的整体大小就是最大对齐数(含嵌套结构体的对齐数)的整数倍。

在这里,嵌套的结构体就是s1,结构体s1的三个结构体成员的对齐数前面已经已经讲过,分别是0、1、4,那么这个嵌套的结构体的最大对齐数就是4,所以,这个嵌套的结构体要对齐到这个最大对齐数也就是4的整数倍处。

现在,我来存储结构体s3。结构体s3的第一个结构体成员d1存放在偏移量为0的位置。
请添加图片描述
如上面内存图,结构体s3的第一个结构体成员d1存放在偏移量为0的位置。

接下来,我来存储结构体s3的第二个结构体成员t,它的类型是struct s1,在前面已经讲过这个嵌套结构体的最大对齐数是4,所以t要对齐到4的整数倍的偏移处。

在内存图寻找,毫无疑问在偏移量为4的位置开始存储,并且这个嵌套结构体是8个字节的大小(在前面已经讲过结构体s1的大小是8个字节),所以要存储8个字节。
请添加图片描述
如上面的内存图,t从偏移量为4的位置开始存储,并且需要存储8个字节的空间。

接下来,我来存储结构体s3的第三个结构体成员r,我依然先求出结构体成员i的对齐数,我使用的是vs编辑器,默认对齐数是8,而结构体成员r的类型是int类型,自身大小是4个字节,在默认对齐数和结构体成员的自身大小选出较小值作为对齐数,毫无疑问是4,那么结构体成员r就要对齐到对齐数也就是4的整数倍处。偏移量12刚好是对齐数的整数倍,所以结构体成员r要从偏移量为12开始存储,大小是4个字节。
请添加图片描述
结构体成员r从偏移量为12开始存储,共存储4个字节。

现在我们需要找出对齐数的最大值,判断此时的结构体大小是否满足是最大对齐数的整数倍,结构体成员d1的对齐数是0,结构体成员t的对齐数是4,结构体成员r的对齐数也是4,所以最大对齐数(含嵌套的结构体的对齐数)是4。
请添加图片描述
此时结构体s3的大小是16个字节,刚好满足是最大对齐数的整数倍,所以结构体s3的最终大小是16个字节,与前面sizeof求出来的结果是一样的。

直到现在,我已经对结构体的内存对齐的规则讲解清楚,并且对于各种情况都有举例子。



结构体对齐的原因(大部分资料提到)

1.平台原因(移植原因):
不是所有的硬件平台都能访问任意地址上的任意数据;某些硬件平台只能在某些地址处取某些特定类型的数据,否则抛出硬件异常。
2.性能原因:
数据结构(由其是栈)应该尽可能地在自然边界上对齐。原因在于,访问未对齐的内存,处理器需要作两次内存访问;而对齐的内存访问仅需要一次访问。
总的来说:结构体的内存对齐是拿空间换取时间的做法。
如:

struct s
{
    char c;
	int i;
};

在上面的结构体中,如果我没有进行内存对齐的话,是下面这样存储的。
请添加图片描述
结构体s中的结构体成员c和结构体成员i是紧密的存储在内存中的,当程序要读取时会出现怎么样的情况的?

假设是在32位平台下进行数据的读取的,那么在32位平台下,一次数据读取是可以读取4个字节的,现在我将不对齐的情况下的进行模拟读取。
请添加图片描述
由图片可以得知,在此次内存读取中,第一次读取包括了结构体成员c和结构体成员i的前三个字节空间,第二次读取了结构体成员i的最后一个字节和往后的三个空字节。

注意在这里第二个结构体成员i的起始地址卡在了第一次读取的位数。

我们可以明显发现,在结构体s没有进行内存对齐时,在第一次读取中,找到了结构体成员c的起始地址,而在第二次读取中,并没有读取到结构体成员i的起始位置。

接下来,我来讲解内存对齐了的情况。
请添加图片描述
结构体s按照结构体内存对齐的规则存储在内存后,就是上面的内存图的情况。接下来,我还是按照在32位平台下,一次读取4个字节的情况来读取内存。
请添加图片描述
在第一次读取中,读取到结构体成员c和三个字节,第二次读取中读取到了结构体成员变量i。

我们可以明显发现,在结构体s进行内存对齐后,在第一次读取中,读取到了结构体成员c的起始位置,在第二次读取中,读取到了结构体成员变量i的起始位置。

总结:结构体的内存对齐,可以让每次读取时找到结构体成员变量的起始位置,如果没有进行内存对齐,那么起始地址可能卡在了每次读取位数的中间(如上面提到的没有对齐情况),那么计算机就需要额外的计算,所以,内存对齐是一种以空间换取空间的做法。

开辟结构体时节省空间的技巧

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

运行结果如下:
请添加图片描述
在介绍结构体的内存对齐规则时,我已经讲解了这两个结构体的大小的由来,现在,我依然使用这两个例子来讲解开辟结构体节省空间的技巧。

观察可以发现,结构体s1和结构体s2都是两个char类型的数据和一个int类型的数据,但是它们的所占用的空间大小却是不相同的。

由此可见,可以使用开辟结构体的技巧,让结构体既保持内存对齐,又可以节省空间。

这个技巧就是,让占用空间小的成员尽量集合在一起。

如结构体s1中,结构体成员有char类型和int类型的两种类型,char类型是占用空间较小的类型,所以c1、c2聚集在一起,而结构体s2并没有这样做,所以结构体s2占用空间较大。



修改默认对齐数

在介绍修改默认对齐数时,我依然通过举例子来进行讲解。

#include<stdio.h>
#pragma pack(1)             //修改默认对齐数为1
struct s1
{
	char c1;
	char c2;
	int i;
};
#pragma pack()             //恢复默认对齐数
int main()
{
	printf("%d\n",sizeof(struct s1));
	return 0;
}

现在,我来存储结构体s1,第一个结构体成员c1存放在偏移量为0的位置。
请添加图片描述
如上面内存图所示,第一个结构体成员c1存放在偏移量为0的位置。

接下来,我来存储第二个结构体成员,我依然要找出它的对齐数,虽然我使用的是vs编辑器,但是默认对齐数不是8,因为在创建结构体时,我已经将默认对齐数修改为1了,而第二个结构体成员的大小是1,所以对齐数为1,那么,第二个结构体成员将从偏移量为1的整数倍处进行存储,所以结构体成员c2将在偏移量为1的位置开始存储。
请添加图片描述
接下来,我来存储第三个结构体成员,我依然要求第三个结构体成员的对齐数。默认对齐数已经被修改为1,而第三个结构体的类型是int类型,大小是4个字节,在默认对齐数和结构体成员自身大小选择较小值,毫无疑问是1,所以第三个结构体成员也对齐到偏移量为1的整数倍处。从偏移量为2开始寻找偏移量是1的整数倍的地方,开始存储结构体成员i,所以,结构体成员i将从偏移量为2的位置开始存储。
请添加图片描述
如上面的内存图,结构体成员i从偏移量为2的位置开始存储,总共4个字节。
请添加图片描述
修改完默认对齐数后,结构体s1的大小是6个字节,是最大对齐数1的整数倍。

现在,我运行一下代码,观察结构体s1的大小是不是6个字节。

运行结果:
请添加图片描述
由运行结果可以得知,在修改默认对齐数为1后,结构体s1的内存大小是6个字节。



结构体传参

在结构体传参中,存在着两种方法,一种是传结构体的地址,一种是直接传整个结构体。

1.传整个结构体

#include<stdio.h>
struct s                                      //创建结构体
{
    int data[1000];
	int num;
};
void printf1(struct s s1)                    //传参整个结构体
{
    int i = 0;
	for(i=0;i<5;i++)
	{
	    printf("%d ",s1.data[i]);
	}
	printf("\n%d\n",s1.num);
}
int main()
{
	struct s s1 = {{1,2,3,4,5},100};            //创建结构体变量
	printf1(s1);
	return 0;
}

运行结果:
请添加图片描述
2.传结构体的地址

#include<stdio.h>
struct s                                      //创建结构体
{
    int data[1000];
	int num;
};
void printf2(struct s* s1)                    //传结构体的地址
{
    int i = 0;
	for(i=0;i<5;i++)
	{
	    //printf("%d ",(*s1).data[i]);      //结构体指针的两种写法
		printf("%d ",s1->data[i]);
	}
	//printf("\n%d\n",(*s1).num);
	printf("\n%d\n",s1->num);
}
int main()
{
	struct s s1 = {{1,2,3,4,5},100};            //创建结构体变量
	printf2(&s1);
	return 0;
}

运行结果:
请添加图片描述
在上面我已经提出了结构体传参的两种方式,那么在结构体传参的过程中,我们应该选择哪一种的传参方式呢?我们应该选择传结构体地址的方式

因为函数在传参的时候,参数是需要压栈的,会有时间和空间上的系统开销。如果传递一个结构体对象的时候,结构体过大,参数压栈的系统开销比较大,所以会导致性能下降。



位段

位段的简介

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

比如:

#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));
	return 0;
}

观察可以发现,在上面例子中,位段与结构体的书写方式相似,唯一不同的是位段的成员名后面加了个冒号和数字,那么这些数字是表示什么呢?别急,在后面讲解位段的内存分配时会讲到。



位段的内存分配

1.位段的成员是int、unsigned int、signed int或char(属于整形家族)类型
2.位段的空间按照4个字节(int)或者1个字节(char)的方式进行开辟的。
3.位段涉及很多不确定因素,位段是不跨平台的,注重可移植的程序应该避免使用位段

在上面介绍位段时,我已经举了一个位段的例子,现在我来讲解这个位段的内存开辟的问题。

#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));
	return 0;
}

在前面我留下了一个问题,就是位段的成员后面加的数字是什么意思呢?现在我来回答,该数字就是这个位段成员所能存放的空间大小,单位是比特位。

该位段的成员a后面加的数字是2,那么变量a只能存放2个比特位
该位段的成员b后面加的数字是5,那么变量b只能存放5个比特位
该位段的成员c后面加的数字是10,那么变量c只能存放10个比特位
该位段的成员d后面加的数字是30,那么变量d只能存放30个比特位

各个位段成员的变量所能存放的比特位大小已经说明清楚,那么现在就需要开辟空间,而这个空间需要开辟多大呢?根据位段内存分配提到的第二条规则,位段的空间按照4个字节(int)或者1个字节(char)的方式进行开辟的。而第一个位段成员是a,类型是int类型,所以最开始要开辟4个字节的空间,也就是32个比特位。

接下来,就是分配空间的问题了。

现在已经开辟了32个比特位,变量a先用去了2个比特位,剩下30个比特位
变量b用去了5个比特位,剩下25个比特位
变量c用去了10个比特位,剩下15个比特位
变量d要用30个比特位,但是只剩下了15个比特位,舍弃15个比特位,因为d也是int类型,所以重新开辟4个字节空间,也就是32比特位,用去30个比特位。

在上面的分析中,程序总共开辟了两次空间,每一次都是4个字节,所以这个位段的大小是8个字节。

运行结果:
请添加图片描述

由运行结果可以得知,我们的猜想是正确的。

至于变量d在面对变量c存储后剩下的空间(剩下15个比特位)不够使用时,直接舍弃该空间,重新开辟。但是,需要注意的是vs编辑器环境下的才是舍弃,在其他的平台是舍弃还是继续使用,是不确定的。

现在我来举一个例子,证明在vs编辑器环境下是舍弃的。

#include<stdio.h>
struct s
{
    char a : 3;
	char b : 4;
	char c : 5;
	char d : 4;
};
int main()
{
    printf("%d\n",sizeof(struct s));
	return 0;
}

运行结果:

请添加图片描述
观察位段的代码获取各个成员的所能存储的比特位。

变量a只能存放3个比特位的空间
变量b只能存放4个比特位的空间
变量c只能存放5个比特位的空间
变量d只能存放4个比特位的空间

现在,先开辟1个字节的空间,也就是8个比特位的空间。假设在变量在每一次存储中,空间不够是直接舍弃,重新开辟。
请添加图片描述
现在开始存储变量a。位段成员是从低地址到高地址进行存储。
请添加图片描述
现在,开始存储变量b。
请添加图片描述
变量c要存储5个比特位,在存储完变量b后,空间不够变量c存储,直接舍弃,重新开辟1个字节的空间。
请添加图片描述
变量d要存储4个比特位,在存储完变量c后,空间不够变量d存储,依然是舍弃该空间,重新开辟1个字节的空间。
请添加图片描述
直到现在,所有的变量已经存储完成,观察图片可知,总共开辟了3个字节的空间,与运行结果相同,所以我们的猜想是正确的。在vs编辑器的环境下,当第二个成员所占用的内存较大时,第一个成员利用后剩余的空间是舍弃而不是继续使用。

位段何时以4个字节开辟空间,何时以1个字节开辟空间
在位段的内存对齐规则中的第二条,位段的空间按照4个字节(int)或者1个字节(char)的方式进行开辟的。

我现在来举个例子,加强对这句话的理解。

#include<stdio.h>
struct A
{
    int a : 10;
	int b : 10;
	int c : 15;
	char d : 5;
	char e : 3;
};
int main()
{
    printf("%d\n",sizeof(struct A));
	return 0;
}

运行结果:
请添加图片描述
现在,开始存储结构体A的成员变量。

首先变量a是int类型,所以开辟4个字节的空间,也就是32个比特位,变量a存储用去了10个比特位,剩下22个比特位。
变量b用去了10个比特位,剩下了12个比特位。
变量c要存储15个比特位,而存储完变量b后只剩下12个比特位,空间不够,直接舍弃,因为变量c是int类型,所以重新开辟4个字节的空间,也就是32个比特位,存储变量c用去了15个比特位,剩下17个比特位。
虽然存储变量c后剩下的17个比特位足够存储变量d,但是变量d是char类型,所以重新开辟1个字节的空间,也就是8个比特位,存储变量d用去了5个比特位,剩下3个比特位。
变量e存储用去了3个比特位。

现在总共开辟了9个字节的空间,最大对齐数是4,对齐到最大对齐数的整数倍,所以总大小是12个字节。

前面已经讲过,位段的空间按照4个字节(int)或者1个字节(char)的方式进行开辟的。

在第一次开辟空间或者内容不足,重新开辟空间时,都要看类型进行开辟空间的,如果是int类型,那么就开辟4个字节的空间,如果是char类型,就开辟1个字节的空间。
并且如果前面都是int类型,中间来了个char类型,即使空间足够存储char类型的变量,也要重新开辟1个字节的空间进行存储。

位段跨平台问题

位段不能跨平台的原因:
1.int位段被当作有符号数还是无符号数,尚未确定。
2.不同机器的最大位数目不能确定。
3.位段中的成员在内存中存放时是从左往右分配还是从右往左分配未定义(vs中从右往左分配即从低地址到高地址,一般右是低地址)。
4,当第二个成员所占用的内存较大,第一个位段中成员利用后剩余的空间是舍弃还是利用(vs中是舍弃)。

总结:位段和结构体相比,位段可以达到同样的效果,而且可以很好的节省空间,但是有跨平台的问题所在。

位段的应用

数据在传输的时候,需要经过多层打包,如果使用位段,不仅节省空间,还提高了传输速度。(含图片)

位段和结构体的是两个极端的存在,结构体是为了效率而牺牲空间,也就是要对齐,而位段是为了节省空间,不用对齐。



枚举

枚举顾名思义就是一一列举,把可能的取值一一列举出来,比如:星期、性别、月份。

枚举的定义

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

运行结果:
请添加图片描述
如上面的代码,利用枚举声明了三个颜色,第一个颜色赋值常量0、第二个颜色赋值常量1、第三个颜色赋值常量2。

现在,我提出一个问题,能不能在主函数里面修改枚举成员的常量?

#include<stdio.h>
enum Color
{
    RED,
	GREEN,
	BLUE
};
int main()
{
	RED = 3;                       //修改枚举成员的常量值
    printf("%d\n",RED);
	printf("%d\n",GREEN);
	printf("%d\n",BLUE);
	return 0;
}

请添加图片描述
在vs的编辑器下,报错。

那么,如果我在定义枚举时,进行修改,是否符合语法呢?

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

运行结果:
请添加图片描述
在vs编辑器中,代码正常跑动起来,证明这种枚举定义时,对枚举成员进行赋值符合语法的。并且后面的枚举常量会自动更新,保证当前的枚举常量的值是前一个枚举常量的值加1。

如果我将枚举常量的成员第一个赋值5,第二个赋值7,第三个赋值9,让它们都不是前一个枚举常量的值加1,程序是否跑得起来呢?

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

运行结果:
请添加图片描述
程序正常运行,证明这种做法是可行的。

枚举的优点

我们可以用#define 来定义常量,为什么非要使用枚举呢?我举个例子,你就懂了。

假设我要声明一整个星期的常量。
采用#define 进行定义:

#define MON 1
#define TUE 2
#define WED 3
#define THU 4
#define FRI 5
#define SAT 6
#define SUN 7

采用枚举进行定义:

enum DAY
{
    NUM,
	TUE,
	WED,
	THU,
	FRI,
	SAT,
	SUN
};

相对而言,枚举相对于#define 一个一个定义,是比较方便的。特别是当需要定义的变量数目的增加,枚举的优势将会放大。

枚举的优点:
1.增加代码的可读性和可维护性。
2.和#define定义的标识符相比较,枚举有类型检查,更加严谨。
3.防止了命名污染(封装),因为使用枚举,那么这个名字只有一个,因为它有自己的类型。
4.便于调试
5.使用方便,一次可以定义多个变量。



联合体(共用体)

联合体类型的定义

联合体也是一种特殊的自定义类型,这种类型定义的变量也包含一系列的成员,特征是这些成员共用同一块空间(所以联合体也叫共用体),空间大小至少是最大成员的大小(因为联合至少得有能力保存最大的那个成员)。

下面我来定义一个联合体。

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

运行结果:
请添加图片描述

联合体的内存存储

#include<stdio.h>
union Un                              //定义一个联合体
{
    char c;
	int i;
	double d;
};
int main()
{
	union Un un;
	printf("%p\n",&un);               //求联合体变量的地址
	printf("%p\n",&(un.c));           //求联合体变量中c的地址
	printf("%p\n",&(un.i));           //求联合体变量中i的地址
	printf("%p\n",&(un.d));           //求联合体变量中d的地址
    return 0;
}

运行结果:
请添加图片描述
由运行结果可以得知,联合体变量、联合体变量中的c、i、d起始地址都是一样的,证明了联合体中的成员是共用同一块地址。

如下面内存图:
请添加图片描述
在这里会不会有人疑惑,联合体中的成员共用一块空间,那么在读取数据时,会不会出现混乱呢?其实,在联合体的使用中,在每一次调用联合体时,只会使用其中的一个成员。

如:在调用联合体变量un,使用c成员时,就不使用i、d成员。
使用i成员,就不使用c、d成员。
使用d成员,就不使用i、c成员。

联合体大小的计算

联合体的大小至少是最大成员的大小。
当最大成员的大小不是最大对齐数的整数倍时,就要对齐到最大对齐数的整数倍处。

在下面的联合体中,大小是多少?

#include<stdio.h>
union Un
{
	char arr[5];
	int i;
};
int main()
{
    printf("%d\n",sizeof(union Un));
	return 0;
}

联合体中,成员arr是5个字节的空间
成员i是4个字节空间
那么最大的结构体成员大小是5
arr的类型是char类型,大小是1个字节,vs默认对齐数是8,那么结构体成员arr对齐数是1
i的类型是int类型,大小是4个字节,vs默认对齐数是8,那么结构体成员i的对齐数是4
联合体的最大对齐数是4
最大结构体成员的大小也就是5不是最大对齐数4的整数倍
所以对齐到最大对齐数的整数倍处,也就是4的整数倍数
综上,联合体的大小是8

运行结果:
请添加图片描述
由运行结果可以得知,我们的分析是正确的。

联合体的用处

利用联合体写一个判断大小端字节存储模式的顺序。

#include<stdio.h>
union Un
{
    char c;
	int i;
}un;
int main()
{
	char ret = 0;
    un.i = 1;
	ret = un.c;
	if(ret == 1)
	{
	    printf("小端\n");
	}
	else
	{
		printf("大端\n");
	}
	return 0;
}

请添加图片描述
在vs运行此串代码,可以得知vs编辑器是小端。

原理如下:
请添加图片描述

00000001,如果数字的低位存储在低地址,那么就是小端字节存储模式。

如下:

请添加图片描述

00000001,如果数字的高位存储在低地址,就是大端字节存储模式。

如下:
请添加图片描述

直到现在,自定义类型就全部讲解完成了,如果对你有帮助的话,关注点一点,下期更精彩。

评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值