C语言高级专题(10)------- 共用体和枚举


共用体union


1、共用体类型的定义、变量定义和使用

  • (1)共用体union和结构体struct在类型定义、变量定义、使用方法上很相似。

  • (2)共用体和结构体的不同:结构体类似于一个包裹,结构体中的成员彼此是独立存在的,分布在内存的不同单元中,他们只是被打包成一个整体叫做结构体而已;共用体中的各个成员其实是一体的,彼此不独立,他们使用同一个内存单元。可以理解为:有时候是这个元素,有时候是那个元素。更准确的说法是同一个内存空间有多种解释方式。

实践代码

#include <stdio.h>


// 结构体类型的定义
struct mystruct
{
	int a;
	char b;
};

// 共用体类型的定义
union myunion
{
	int a;
	char b;
};

int main(void)
{

	struct mystruct s1;
	s1.a = 23;
	printf("s1.b = %d.\n", s1.b);		// s1.b = 0. 结论是s1.a和s1.b是独立无关的
	printf("&s1.a = %p.\n", &s1.a);     //0xbf9dd198
	printf("&s1.b = %p.\n", &s1.b);     //0xbf9dd19c 
	
	union myunion u1;		            // 共用体变量的定义
	u1.a = 23;				            // 共用体元素的使用
	printf("u1.b = %d.\n", u1.b);		// u1.b = 23.结论是u1.a和u1.b是相关的
	
	// a和b的地址一样,充分说明a和b指向同一块内存,只是对这块内存的不同解析规则
	printf("&u1.a = %p.\n", &u1.a);     //0xbf9dd194
	printf("&u1.b = %p.\n", &u1.b);     //0xbf9dd194

	return 0;
	
}

在这里插入图片描述

  • (3)共用体union就是对同一块内存中存储的二进制的不同的理解方式。
    a和b其实指向同一块内存空间,只是对这块内存空间的2种不同的解析方式。如果我们使用u1.a那么就按照int类型来解析这个内存空间;如果我们使用u1.b那么就按照char类型 来解析这块内存空间。

  • (4)union的sizeof测到的大小实际是union中各个元素里面占用内存最大的那个元素的大小。因为可以存的下这个就一定能够存的下其他的元素。

  • (5)union中的元素不存在内存对齐的问题,因为union中实际只有1个内存空间,都是从同一个地址开始的(开始地址就是整个union占有的内存空间的首地址),所以不涉及内存对齐。

2、共用体和结构体的相同和不同

  • (1)相同点就是操作语法几乎相同。
  • (2)不同点是本质上的不同。struct是多个独立元素(内存空间)打包在一起;union是一个元素(内存空间)的多种不同解析方式。

3、共用体的主要用途

  • (1)共用体就用在那种对同一个内存单元进行多种不同规则解析的这种情况下。
  • (2)C语言中其实是可以没有共用体的,用指针和强制类型转换可以替代共用体完成同样的功能,但是共用体的方式更简单、更便捷、更好理解。
#include <stdio.h>
union test
{
	int a;
	float b;
};

int main(void)
{
	union test t1;
	t1.a = 1123477881;           
	printf("value = %f.\n", t1.b);     //用float的格式去解析int型数据,得到123.456001
	
	int a = 1123477881;
	printf("指针方式:%f.\n", *((float *)&a));   //用指针方式,强制转换也可以达到同样的效果,得到123.456001
	
	return 0;
}

4、什么是大小端模式

  • (1)大端模式(big endian)和小端模式(little endian)。

  • (2)计算机通信发展起来后,遇到一个问题就是:在串口等串行通信中,一次只能发送1个字节。这时候我要发送一个int类型的数就遇到一个问题。int类型有4个字节,我是按照:byte0 byte1 byte2 byte3这样的顺序发送,还是按照byte3 byte2 byte1 byte0这样的顺序发送。规则就是发送方和接收方必须按照同样的字节顺序来通信,否则就会出现错误。这就叫通信系统中的大小端模式。这是大小端这个词和计算机挂钩的最早问题。

  • (3)现在我们讲的这个大小端模式,更多是指计算机存储系统的大小端。在计算机内存/硬盘/Nnad中。因为存储系统是32位的,但是数据仍然是按照字节为单位的。于是乎一个32位的二进制在内存中存储时有2种分布方式:高字节对应高地址(小端模式)、高字节对应低地址(大端模式)

在这里插入图片描述

  • (4)大端模式和小端模式本身没有对错,没有优劣,理论上按照大端或小端都可以,但是要求必须存储时和读取时按照同样的大小端模式来进行,否则会出错。
  • (5)现实的情况就是:有些CPU公司用大端(譬如C51单片机);有些CPU用小端(譬如ARM)。(大部分是用小端模式,大端模式的不算多)。于是乎我们写代码时,当不知道当前环境是用大端模式还是小端模式时就需要用代码来检测当前系统的大小端。

5、用union来测试机器的大小端模式
经典笔试题:用C语言写一个函数来测试当前机器的大小端模式。

#include <stdio.h>

// 共用体中很重要的一点:a和b存放都是从u1的低地址开始的。
// 假设u1所在的4字节地址分别是:0、1、2、3的话,那么a存放顺序自然就是0、1、2、3;
// b所在的地址是0而不是3.
union myunion
{
	int a;
	char b;
};

// 如果是小端模式则返回1,小端模式则返回0
int is_little_endian(void)
{
	union myunion u1;         //1对应十六进制就是0x0000_0001
	u1.a = 1;				// 地址0的那个字节内是1(小端)或者0(大端)
	return u1.b;
}

int main(void)
{
	int i = is_little_endian();
	if (i == 1)
	{
		printf("小端模式\n");      //在X86下的Linux中验证是小端模式
	}
	else
	{
		printf("大端模式\n");
	}
	
	return 0;                      
}

6、指针方式来测试机器的大小端

#include <stdio.h>

// 共用体中很重要的一点:a和b存放都是从u1的低地址开始的。
// 假设u1所在的4字节地址分别是:0、1、2、3的话,那么a存放顺序自然就是0、1、2、3;
// b所在的地址是0而不是3.
union myunion
{
	int a;
	char b;
};

// 如果是小端模式则返回1,小端模式则返回0
int is_little_endian2(void)
{
	int a = 1;
	char b = *((char *)(&a));		// 指针方式其实就是共用体的本质
	
	return b;
}

int main(void)
{
	int i = is_little_endian2();
	if (i == 1)
	{
		printf("小端模式\n");      //在X86下的Linux中验证是小端模式
	}
	else
	{
		printf("大端模式\n");
	}
	
	return 0;                      
}

7、看似可行实则不行的测试大小端方式:位与、移位、强制类型转化
(1)位与运算。
结论:位与的方式无法测试机器的大小端模式。(表现就是大端机器和小端机器的&运算后的值相同的)
理论分析:位与运算是编译器提供的运算,这个运算是高于内存层次的(或者说&运算在二进制层次具有可移植性,也就是说&的时候一定是高字节&高字节,低字节&低字节,和二进制存储无关)。

(2)移位
结论:移位的方式也不能测试机器大小端。
理论分析:原因和&运算符不能测试一样,因为C语言对运算符的级别是高于二进制层次的。右移运算永远是将低字节移除,而和二进制存储时这个低字节在高位还是低位无关的。
(3)强制类型转换
同上


枚举


1、枚举是用来干嘛的?

  • (1)枚举在C语言中其实是一些符号常量集。直白点说:枚举定义了一些符号,这些符号的本质就是int类型的常量,每个符号和一个常量绑定。这个符号就表示一个自定义的一个识别码,编译器对枚举的认知就是符号常量所绑定的那个int类型的数字。

  • (2)枚举符号常量和其对应的常量数字相对来说,数字不重要,符号才重要。符号对应的数字只要彼此不相同即可,没有别的要求。所以一般情况下我们都不明确指定这个符号所对应的数字,而让编译器自动分配。(编译器自动分配的原则是:从0开始依次增加。如果用户自己定义了一个值,则从那个值开始往后依次增加)

(3)验证代码

#include <stdio.h>


// 这个枚举用来表示函数返回值,ERROR表示错,RIGHT表示对
enum return_value
{
	ERROR,				// 枚举值常量是全局的,直接自己就可以用。
	RIGHT,
};

enum return_value func1(void);

int main(void)
{
	enum return_value r = func1();

	if (r == RIGHT)			// 不是r.RIGHT,也不是return_value.RIGHT,跟结构体的使用方法不一样
	{
		printf("函数执行正确\n");
	}
	else
	{
		printf("函数执行错误\n");
	}
	
	printf("ERROR = %d.\n", ERROR);     //  0
	printf("RIGHT = %d.\n", RIGHT);    //   1
	
	
	return 0;
}

enum return_value func1(void)
{
	enum return_value r1;
	r1 = ERROR;
	return r1;
}

2、C语言为何需要枚举

  • (1)C语言没有枚举是可以的。使用枚举其实就是对1、0这些数字进行符号化编码,这样的好处就是编程时可以不用看数字而直接看符号。符号的意义是显然的,一眼可以看出。而数字所代表的含义除非看文档或者注释。
  • (2)宏定义的目的和意义是:不用数字而用符号。从这里可以看出:宏定义和枚举有内在联系。宏定义和枚举经常用来解决类似的问题,他们俩基本相当可以互换,但是有一些细微差别。

3、宏定义和枚举的区别

  • (1)枚举是将多个有关联的符号封装在一个枚举中,而宏定义是完全散的。也就是说枚举其实是多选一。
  • (2)什么情况下用枚举?当我们要定义的常量是一个有限集合时(譬如一星期有7天,譬如一个月有31天,譬如一年有12个月····),最适合用枚举。(其实宏定义也行,但是枚举更好)
  • (3)不能用枚举的情况下(定义的常量符号之间无关联,或者无限的)用宏定义。

总结:宏定义先出现,用来解决符号常量的问题;后来人们发现有时候定义的符号常量彼此之间有关联(多选一的关系),用宏定义来做虽然可以但是不贴切,于是乎发明了枚举来解决这种情况。

4、枚举的定义和使用

#include "stdio.h"

/*
 ****************************************************************
 * 	enumeration 类型定义
 ****************************************************************
 */ 
 
	// 定义方法1,定义类型和定义变量分离开
enum week
{
	SUN,		// SUN = 0
	MON,		// MON = 1;
	TUE,
	WEN,
	THU,
	FRI,
	SAT,
};

enum week today;


		// 定义方法2,定义类型的同时定义变量
enum week
{
	SUN,		// SUN = 0
	MON,		// MON = 1;
	TUE,
	WEN,
	THU,
	FRI,
	SAT,
}today,yesterday;


	// 定义方法3,定义类型的同时定义变量
enum 
{
	SUN,		// SUN = 0
	MON,		// MON = 1;
	TUE,
	WEN,
	THU,
	FRI,
	SAT,
}today,yesterday;


	// 定义方法4,用typedef定义枚举类型别名,并在后面使用别名进行变量定义
typedef enum week
{
	SUN,		// SUN = 0
	MON,		// MON = 1;
	TUE,
	WEN,
	THU,
	FRI,
	SAT,
}week;


	// 定义方法5,用typedef定义枚举类型别名,并在后面使用别名进行变量定义
typedef enum 
{
	SUN,		// SUN = 0
	MON,		// MON = 1;
	TUE,
	WEN,
	THU,
	FRI,
	SAT,
}week;


//错误类型举例
 

// 错误1,枚举类型重名,编译时报错:error: conflicting types for ‘DAY’
typedef enum workday
{
	MON,		// MON = 1;
	TUE,
	WEN,
	THU,
	FRI,
}DAY;

typedef enum weekend
{
	SAT,
	SUN,
}DAY;


	// 错误2,枚举成员重名,编译时报错:redeclaration of enumerator ‘MON’
typedef enum workday
{
	MON,		// MON = 1;
	TUE,
	WEN,
	THU,
	FRI,
}workday;

typedef enum weekend
{
	MON,
	SAT,
	SUN,
}weekend;

// 结构体中元素可以重名
typedef struct 
{
	int a;
	char b;
}st1;

typedef struct 
{
	int a;
	char b;
}st2;


	// #define宏可以重复定义(没有error但是有warning),结果以最后一次定义为准。
#define MACRO1	12
#define MACRO1	24

//	测试代码
 
int main(int argc, char **argv)
{
	printf("%d\n", MACRO1);


  		// 测试定义方法4,5
	week today;
	today = WEN;
	printf("today is the %dth day in week\n", today);


	// 测试定义方法2
	today = WEN;
	printf("today is the %dth day in week\n", today);



	// 测试enum变量的类型
	enum week w1;
	w1 = TUE;
	printf("%d\n", w1);

测试结论记录


1、编译以下代码

	enum week w1;		
	w1 = TUE;		
	printf("%s\n", w1);		
  • 出现警告,enum.c:28: warning: format ‘%s’ expects type ‘char *’, but argument 2 has type ‘unsigned int’
  • 分析:典型的格式化输出和类型不匹配,从警告信息可以看出enum变量的类型为unsigned int既然是unsigned int,那自然使用%d打印是正确的了。

2、不能有重名的枚举类型。即在一个文件中不能有两个或两个以上的enum被typedef成相同的别名。

  • 分析:这很好理解,因为将两种不同类型重命名为相同的别名,这会让gcc在还原别名时遇到困惑。比如你定义了
    typedef int INT; typedef char INT;那么INT到底被译为int还是char呢?

3、不能有重名的枚举成员。

  • 分析:经过测试,两个struct类型内的成员名称可以重名,而两个enum类型中的成员不可以重名。实际上从两者的成员在访问方式上的不同就可以看出了。
  • struct类型成员的访问方式是:变量名.成员,而enum成员的访问方式为:成员名。因此若两个enum类型中有重名的成员,那代码中访问这个成员时到底指的是哪个enum中的成员呢?
  • 两个#define宏定义是可以重名的,该宏名真正的值取决于最后一次定义的值。编译器会给出警告但不会error

  • 0
    点赞
  • 4
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值