网上学习资料一大堆,但如果学到的知识不成体系,遇到问题时只是浅尝辄止,不再深入研究,那么很难做到真正的技术提升。
一个人可以走的很快,但一群人才能走的更远!不论你是正从事IT行业的老鸟或是对IT行业感兴趣的新人,都欢迎加入我们的的圈子(技术交流、学习资源、职场吐槽、大厂内推、面试辅导),让我们一起学习成长!
这篇文章主要讲解一下联合体和枚举的基础知识,使用场景和使用方法,以便我们可以更好的了解联合体的使用方法。
二、联合体
1.联合体基础
当多个数据需要共享内存或者多个数据每次只取其一时,可以利用联合体(union)。在C Programming Language 一书中对于联合体是这么描述的:
1)联合体是一个结构;
2)它的所有成员相对于基地址的偏移量都为0;
3)此结构空间要大到足够容纳最"宽"的成员;
4)其对齐方式要适合其中所有的成员;
下面解释这四条描述:
由于联合体中的所有成员是共享一段内存的,因此每个成员的存放首地址相对于于联合体变量的基地址的偏移量为0,即所有成员的首地址都是一样的。为了使得所有成员能够共享一段内存,因此该空间必须足够容纳这些成员中最宽的成员。对于这句“对齐方式要适合其中所有的成员”是指其必须符合所有成员的自身对齐方式。
下面举例说明:
如联合体
union U
{
char s[9];
int n;
double d;
};
s占9字节,n占4字节,d占8字节,因此其至少需9字节的空间。然而其实际大小并不是9,用运算符sizeof测试其大小为16.这是因为这里存在字节对齐的问题,9既不能被4整除,也不能被8整除。因此补充字节到16,这样就符合所有成员的自身对齐了。从这里可以看出联合体所占的空间不仅取决于最宽成员,还跟所有成员有关系,即其大小必须满足两个条件:1)大小足够容纳最宽的成员;2)大小能被其包含的所有基本数据类型的大小所整除。
测试程序:
/\*测试联合体 2011.10.3\*/
#include <iostream>
using namespace std;
union U1
{
char s[9];
int n;
double d;
};
union U2
{
char s[5];
int n;
double d;
};
int main(int argc, char \*argv[])
{
U1 u1;
U2 u2;
printf("%d\n",sizeof(u1));
printf("%d\n",sizeof(u2));
printf("0x%x\n",&u1);
printf("0x%x\n",&u1.s);
printf("0x%x\n",&u1.n);
printf("0x%x\n",&u1.d);
u1.n=1;
printf("%d\n",u1.s[0]);
printf("%lf\n",u1.d);
unsigned char \*p=(unsigned char \*)&u1;
printf("%d\n",\*p);
printf("%d\n",\*(p+1));
printf("%d\n",\*(p+2));
printf("%d\n",\*(p+3));
printf("%d\n",\*(p+4));
printf("%d\n",\*(p+5));
printf("%d\n",\*(p+6));
printf("%d\n",\*(p+7));
return 0;
}
输出结果为:
16
8
0x22ff60
0x22ff60
0x22ff60
0x22ff60
1
0.000000
1
0
0
0
48
204
64
0
请按任意键继续. . .
对于sizeof(u1)=16。因为u1中s占9字节,n占4字节,d占8字节,因此至少需要9字节。其包含的基本数据类型为char,int,double分别占1,4,8字节,为了使u1所占空间的大小能被1,4,8整除,则需填充字节以到16,因此sizeof(u1)=16.
对于sizeof(u2)=8。因为u2中s占5字节,n占4字节,d占8字节,因此至少需要8字节。其包含的基本数据类型为char,int,double分别占1,4,8字节,为了使u2所占空间的大小能被1,4,8整除,不需填充字节,因为8本身就能满足要求。因此sizeof(u2)=8。
从打印出的每个成员的基地址可以看出,联合体中每个成员的基地址都相同,等于联合体变量的首地址。
对u1.n=1,将u1的n赋值为1后,则该段内存的前4个字节存储的数据为00000001 00000000 00000000 00000000
因此取s[0]的数据表示取第一个单元的数据,其整型值为1,所以打印出的结果为1.
至于打印出的d为0.000000愿意如下。由于已知该段内存前4字节的单元存储的数据为00000001 00000000 00000000 00000000,从上面打印结果48,204,64,0可以知道后面4个字节单元中的数据为00110000 11001100 01000000 00000000,因此其表示的二进 制浮点数为
00000000 01000000 11001100 00110000 00000000 00000000 00000000 00000001
对于double型数据,第63位0为符号位,62-52 00000000100为阶码,0000 11001100 00110000 00000000 00000000 00000000 00000001为尾数,根据其值知道尾数值约为0,而阶码为4-1023=-1019,因此其表示的浮点数为1.0*2^(-1019)=0.00000000000…,因此输出结果为0.000000。
2. 联合体的使用方法和使用场景
在某种程度上类似结构体struct的一种数据结构,共用体(union)和结构体(struct)同样可以包含很多种数据类型和变量。
不过区别也挺明显:
结构体(struct)中所有变量是“共存”的——优点是“有容乃大”,全面;缺点是struct内存空间的分配是粗放的,不管用不用,全分配。
而联合体(union)中是各变量是“互斥”的——缺点就是不够“包容”;但优点是内存使用更为精细灵活,也节省了内存空间。
2.1 多种访问内存途径共存
一个例子了然:
//example
#include<stdio.h>
union var{
long int l;
int i;
};
main(){
union var v;
v.l = 5;
printf("v.l is %d\n",v.i);
v.i = 6;
printf("now v.l is %ld! the address is %p\n",v.l,&v.l);
printf("now v.i is %d! the address is %p\n",v.i,&v.i);
}
结果:
v.l is 5
now v.l is 6! the address is 0xbfad1e2c
now v.i is 6! the address is 0xbfad1e2c
所以说,管union的叫共用体还真是贴切——完全就是共用一个内存首地址,并且各种变量名都可以同时使用,操作也是共同生效。如此多的access内存手段,确实好用,不过这些“手段”之间却没法互相屏蔽——就好像数组+下标和指针+偏移一样。
上例中我改了v.i的值,结果v.l也能读取,那么也许我还以为v.l是我想要的值呢,因为上边提到了union的内存首地址肯定是相同的,那么还有一种情况和上边类似:
一个int数组变量a,一个long int(32位机中,long int占4字节,与int相同)变量b,我即使没给int变量b赋值,因为数据类型相同,我使用int变量b也完全会拿出int数组a中的a[0]来,一些时候一不小心用上,还以为用的就是变量b呢~
这种逻辑上的错误是很难找出来的(只有当数据类型相去甚远的时候稍好,出个乱码什么的很容易发现错误)。
#include<stdio.h>
union var{
long int l;
int i;
};
int main(){
union var v;
v.l = 5;
printf("%ld\n",v.l);
v.i = 6;
}
2.2 联合体union和大小端(big-endian、little-endian)
下边示范了一种用途,代表四个含义的四个变量,但是可以用一个int来操作,直接int赋值,无论内存访问(指针大小的整数倍,访问才有效率),还是时间复杂度(一次和四次的区别,而且这四次有三次都是不整齐的地址),都会低一些。
#include<stdio.h>
union var{
char c[4];
int i;
};
int main(){
union var data;
data.c[0] = 0x04;//因为是char类型,数字不要太大,算算ascii的范围~
data.c[1] = 0x03;//写成16进制为了方便直接打印内存中的值对比
data.c[2] = 0x02;
data.c[3] = 0x11;
//数组中下标低的,地址也低,按地址从低到高,内存内容依次为:04,03,02,11。总共四字节!
//而把四个字节作为一个整体(不分类型,直接打印十六进制),应该从内存高地址到低地址看,0x11020304,低位04放在低地址上。
printf("%x\n",data.i);
}
结果:
11020304
证明我的32位linux是小端(little-endian)
2.3 联合体union所占内存空间大小
前边说了,首先,union的首地址是固定的,那么,union到底总共有多大?根据一些小常识,做个不严谨不高深的基础版验证吧。
根据:分配栈空间的时候内存地址基本上是连续的,至少同类型能保证在一起,连续就说明,我如果弄三个结构体出来,他们三个地址应该连着,看一下三个地址的间隔就知道了。
#include<stdio.h>
union sizeTest{
int a;
double b;
};
main(){
union sizeTest unionA;
union sizeTest unionB;
union sizeTest unionC;
printf("the initial address of unionA is %p\n",&unionA);
printf("the initial address of unionB is %p\n",&unionB);
printf("the initial address of unionC is %p\n",&unionC);
}
打印,可以看到结果:
the initial address of unionA is 0xbf9b8df8
the initial address of unionB is 0xbf9b8e00
the initial address of unionC is 0xbf9b8e08
很容易看出,8,0,8,这间隔是8字节,按double走的。
怕不保险,再改一下,把int改成数组,其他不变:
union sizeTest{
int a[10];
double b;
};
打印
the initial address of unionA is 0xbfbb7738
the initial address of unionB is 0xbfbb7760
the initial address of unionC is 0xbfbb7788
88-60=28
60-38=28
算错了?我说的可是16进制0x。那么0x28就是40个字节,正好是数组a的大小。
忘了提一个功能——sizeof()
用sizeof直接看,就知道union的大小了
printf("the sizeof of unionA is %d\n",sizeof(unionA));
printf("the sizeof of unionB is %d\n",sizeof(unionB));
printf("the sizeof of unionC is %d\n",sizeof(unionC));
printf("the sizeof of union is %d\n",sizeof(union sizeTest));
上边说的地址规律,没有特定规则,也可能和你的编译器有关。另外,那只是栈空间,还可以主动申请堆空间,当然,堆空间就没有连续不连续一说了。
2.4 联合体union适用场合
有了前边那个验证,基本可以确认,union的内存是照着里边占地儿最大的那个变量分的。
也就可以大胆的推测一下,这种union的使用场合,是各数据类型各变量占用空间差不多并且对各变量同时使用要求不高的场合(单从内存使用上,我觉得没错)。
像上边做的第二个测试,一个数组(或者更大的数组int a[100]),和一个或者几个小变量写在一个union里,实在没什么必要,节省的空间太有限了,还增加了一些风险(最少有前边提到的逻辑上的风险)。所以,从内存占用分析,这种情况不如直接struct。
不过话说回来,某些情况下虽然不是很节约内存空间,但是union的复用性优势依然存在啊,比如方便多命名,这种“二义性”,从某些方面也可能是优势。这种方法还有个好处,就是某些寄存器或通道大小有限制的情况下,可以分多次搬运。
2.5 联合体使用的几个小技巧
联合体可以结合位域一起使用
typedef union {
unsigned char i;
union {
unsigned int a1:1;
unsigned int a2:2;
unsigned int a3:3;
unsigned int a4:2;
}bitField;
}TEST;
TEST test;
//整体赋值
test.i = 0;
//局部赋值
test.bitField.a1 = 1;
test.bitField.a2 = 2;
test.bitField.a3 = 3;
test.bitField.a4 = 0;
这样我们可以方便的整体赋值,也可以根据需要对某个位域进行赋值。
在串口通信中,我们还可以构造这样的联合体来表示接收的数据:
typedef union {
unsigned char arr[32];
union {
unsigned char head1;
unsigned char head2;
unsigned char logo;
unsigned char temp[28];
unsigned char checkSum;
}data;
}COMDATA;
COMDATA comData;
//我们把从串口接收的数据接收到 comData.arr中,在解析数据包时,我们不再需要去记忆包头和检验和的位置,直接使用
comData.data.head1
comData.data.head2
comData.data.checkSum
来表示
这样就比较方便使用了。
2.5 本质&进阶
根据ion固定首地址和union按最大需求开辟一段内存空间两个特征,可以发现,所有表面的定义都是虚的,所谓联合体union,就是在内存给你划了一个足够用的空间,至于你怎么玩它不管!(何止是union和struct,C不就是玩地址么,所以使用C灵活,也容易犯错)
没错,union的成员变量是相当于开辟了几个访问途径(即union包含的变量)!但是,没开辟的访问方式就不能用了?当然也能用!
写个小测试:
#include<stdio.h>
union u{
int i;
double d;//这个union有8字节大小
};
main(){
union u uu;
uu.i = 10;
printf("%d\n",uu.i);
char \* c;
c = (char \*)&uu;//把union的首地址赋值、强转成char类型
c[0] = 'a';
c[1] = 'b';
c[2] = 'c';
c[3] = '\0';
c[4] = 'd';
c[5] = 'e';
//最多能到c[7]
printf("%s\n",c);//利用结束符'\0'打印字符串"abc"
printf("%c %c %c %c %c %c\n",c[0],c[1],c[2],c[3],c[4],c[5]);
}
一个例子了然,我的结构体只定义了int和double“接口”,只要我获得地址,往里边扔什么数据谁管得到?这就是C语言(不止union)的本质——只管开辟一段空间。
但是你获取地址并访问和存取的数据,最好确定是合法(语法)合理(用途符合)的地址,不然虽然能操作,后患无穷,C的头疼之处,可能出了问题你都找不到。
三、枚举
1.枚举基础知识
枚举是 C 语言中的一种基本数据类型,它可以让数据更简洁,更易读。
枚举语法定义格式为:
enum 枚举名 {枚举元素1,枚举元素2,……};
接下来我们举个例子,比如:一星期有 7 天,如果不用枚举,我们需要使用 #define 来为每个整数定义一个别名:
#define MON 1
#define TUE 2
#define WED 3
#define THU 4
#define FRI 5
#define SAT 6
#define SUN 7
这个看起来代码量就比较多,接下来我们看看使用枚举的方式:
enum DAY
{
MON=1, TUE, WED, THU, FRI, SAT, SUN
![img](https://img-blog.csdnimg.cn/img_convert/b6324af9a8f688505f6787ec6f27a975.png)
![img](https://img-blog.csdnimg.cn/img_convert/80db7254fabbc8990fe16f52e3b42740.png)
**既有适合小白学习的零基础资料,也有适合3年以上经验的小伙伴深入学习提升的进阶课程,涵盖了95%以上C C++开发知识点,真正体系化!**
**由于文件比较多,这里只是将部分目录截图出来,全套包含大厂面经、学习笔记、源码讲义、实战项目、大纲路线、讲解视频,并且后续会持续更新**
**[如果你需要这些资料,可以戳这里获取](https://bbs.csdn.net/topics/618668825)**
define MON 1
#define TUE 2
#define WED 3
#define THU 4
#define FRI 5
#define SAT 6
#define SUN 7
这个看起来代码量就比较多,接下来我们看看使用枚举的方式:
enum DAY
{
MON=1, TUE, WED, THU, FRI, SAT, SUN
[外链图片转存中...(img-4kSrjGzv-1715743007575)]
[外链图片转存中...(img-QPhkWNwy-1715743007576)]
**既有适合小白学习的零基础资料,也有适合3年以上经验的小伙伴深入学习提升的进阶课程,涵盖了95%以上C C++开发知识点,真正体系化!**
**由于文件比较多,这里只是将部分目录截图出来,全套包含大厂面经、学习笔记、源码讲义、实战项目、大纲路线、讲解视频,并且后续会持续更新**
**[如果你需要这些资料,可以戳这里获取](https://bbs.csdn.net/topics/618668825)**