参考资料:
C/C++中的联合体
联合体(union)的使用方法及其本质
C语言之联合(union)的妙用

union定义与简单使用

union,中文名“联合体、共用体”,在某种程度上是类似结构体struct的一种数据结构,联合体(union)和结构体(struct)同样可以包含很多种数据类型和变量。
两者区别如下:

  • 结构体(struct)中所有变量是“共存”的——优点是“有容乃大”,全面;缺点是struct内存空间的分配是粗放的,不管用不用,全分配。
  • 联合体(union)中是各变量是“互斥”的——缺点就是不够“包容”,即任何两个成员不会同时有效;但优点是内存使用更为精细灵活,也节省了内存空间。

当多个数据需要共享内存或者多个数据每次只取其一时,可以利用联合体(union)。在C Programming Language 一书中对于联合体是这么描述的:

  1. 联合体是一个结构体;
  2. 它的所有成员相对于基地址的偏移量都为0;
  3. 此结构空间要大到足够容纳最"宽"的成员;
  4. 其内存对齐方式要适合其中所有的成员;

定义形式如下:

union 名称{
	public: //此行可不写,默认为public
		公有成员
	protected:
		保护型成员
	private:
		私有成员
};

无名联合体

union { 
	int i; 
	float f; 
};
i = 5; // 无名联合体可直接用变量名使用
f = 2.0; // 此时i赋值的内容已无效

使用举例:

union Mark{ //表示成绩的联合体,同一门课程只会存在一种成绩表示方法
	char level; // 表示等级制的成绩‘A’ ‘B’ 'C'等
	bool pass; // 只计是否通过课程的成绩 0 1
	int grade; // 表示分数制的成绩
};

使用联合体保存成绩信息,并且输出:(下面代码在vs2013、vs2017正常运行,vs2015上会报错,提示枚举类未定义,感觉是vs2015默认配置的原因)

#include<iostream>
#include<string>

using namespace std;

class ExamInfo {
private:
	string name; //课程名
	enum { LEVEL, PASS, GRADE } mode; //计分方式
	union {
		char level; // 表示等级制的成绩‘A’ ‘B’ 'C'等
		bool pass; // 只计是否通过课程的成绩 
		int grade; // 表示分数制的成绩
	};
public:
	ExamInfo() {};
	ExamInfo(string name, char level) : name(name), mode(ExamInfo::LEVEL), level(level) {};
	ExamInfo(string name, bool pass) : name(name), mode(ExamInfo::PASS), pass(pass) {};
	ExamInfo(string name, int grade) : name(name), mode(ExamInfo::GRADE), grade(grade) {};
	void show() {
		cout << name << ":";
		switch (mode) {
		case ExamInfo::LEVEL: cout << level << endl; break;
		case ExamInfo::PASS: cout << (pass ? "PASS" : "FAIL") << endl; break;
		case ExamInfo::GRADE: cout << grade << endl; break; 
		}
	}
};

int main() {
	ExamInfo course1("English", 'B');
	ExamInfo course2("Math", true);
	ExamInfo course3("C++ Programming", 89);
	course1.show();
	course2.show();
	course3.show();
	system("pause");
	return 0;
}

输入结果:
在这里插入图片描述

内存分配与所占空间

联合体所占的空间不仅取决于最宽成员,还跟所有成员有关系,即其大小必须满足两个条件:
1. 大小足够容纳最宽的成员
2. 大小能被其包含的所有基本数据类型的大小所整除。

测试样例如下:

#include<iostream>  
using namespace std;

int main() {
	union U1 {
		int n;
		char s[11];
		double d;
	};
	union U2 {
		int n;
		char s[5];
		double d;
	};
	U1 u1;
	U2 u2;
	cout << sizeof(u1) << '\t' << sizeof(u2) << endl;
	cout << "u1各数据地址:\n" << &u1 << '\t' << &u1.d << '\t' << &u1.s << '\t' << &u1.n << endl;
	cout << "u2各数据地址:\n" << &u2 << '\t' << &u2.d << '\t' << &u2.s << '\t' << &u2.n << endl;
	system("pause");
	return 0;
}

输出结果:
在这里插入图片描述
对于U1联合体,n占4字节,d占8字节,s的基本类型是char,字节数为1,s最多的时候占用11字节,所以U1至少要11字节,但11字节对于8字节的double类型来说无法整除,所以扩充到16字节。这是因为存在字节对齐的问题,11既不能被4整除,也不能被8整除。因此补充字节到16,这样就符合所有成员的自身对齐了。

对于U2联合体,同理知道其大小为8。
从结果中还可以发现,联合体中的各数据的存储地址都是相同的

多种访问内存途径共存

联合体中数据是共享相同的存储空间,各种变量名都可以同时使用,操作也是共同生效。具体访问方式如下:

#include<iostream>  
using namespace std;

int main() {
	union U {
		unsigned int n;
		unsigned char s[4];
	};
	U u;
	u.n = 0xf1f2f3f4;
	cout << "数值大小:" << hex << u.n << '\t' << "地址:" << &u.n << endl;
	cout << "数值大小:" << hex << (int)u.s[0] << '\t' << "地址:" << (void*)&u.s[0] << endl;
	cout << "数值大小:" << hex << (int)u.s[1] << '\t' << "地址:" << (void*)&u.s[1] << endl;
	cout << "数值大小:" << hex << (int)u.s[2] << '\t' << "地址:" << (void*)&u.s[2] << endl;
	cout << "数值大小:" << hex << (int)u.s[3] << '\t' << "地址:" << (void*)&u.s[3] << endl;
	system("pause");
	return 0;
}

输出结果:
在这里插入图片描述
char s[4]代表四个变量,但是可以用一个int来操作,直接int赋值,无论内存访问(指针大小的整数倍,访问才有效率),还是时间复杂度(一次和四次的区别,而且这四次有三次都是不整齐的地址),都会低一些

优点:多种访问内存的手段可以灵活读取任意部分数据,也可整体进行赋值。在某些寄存器或通道大小有限制的情况下,可以分多次搬运,具体例子见后续内容。
缺点:由于所有变量都能使用,容易使用错误的变量造成逻辑错误。

数据存储位置

接下来研究具体每种类型数值都存储在哪里。上一节的代码中:

int变量 n = 0xf1f2f3f4,在存储时低位f4放在低地址上,故存储顺序为:f4 f3 f2 f1。

char s[4]数组中下标低的,地址也低,按地址从低到高存储。故在输出时顺序为:f4 f3 f2 f1。

因此,当使用数组s读取数据,拼接起来转为Int时,需要反转才能得到正确的数字。

union应用之寄存器读取

设想用C语言实现这样一个功能。我需要用单片机读取一个监控温度的i2c slave的寄存器数据。这个寄存器是12位有效位寄存器。读出来之后我们要通过数据手册给定的公式计算成实际温度(设想这个公式为 temp = reg_val *10)。我们怎么实现呢?要知道,i2c的数据传输是按照byte传送的,也就是说,你只能用char类型结束数据,说白了,每个时序你只能接收8个bit的数据。所以12个bit需要读两次,用两个char类型变量或一个char类型数据接收。
具体步骤如下:

  1. 读出寄存器数据(这个不在这篇文章的讨论范围内)。
  2. 将读出的数据转换成可计算的数据类型(两个char类型转换成一个short或int或float类型)。
  3. 根据公式计算。

下面看一下不用union实现的函数

int fun( void ) {
    int tmp_value = 0; 
    char reg_val[2] = {0,0}; 
    .... 
    i2c.read(addr<<1, reg_val, 2); 
    tmp_value = (reg_val[1]<<8 | reg_val[0]); 
    return tmp_value*10; 
}

用union

union REG_VAL { 
    int value; 
    char buf[2]; 
}reg_val; 
 
int fun( void ) { 
    .... 
    i2c.read(addr<<1, reg_val.buf, 2);
    return reg_val.value*10; 
}

可以看到虽然在这里用union的代码比不用union的多了几行,但是i2c sensor如果多的话,那就会少很多,而且i2c sensor的寄存器有效位数不是一样的,这个用两个char类型就解决了,但是其他的可能需要用三个,所以用最上面定义的union变量可以很好的实现,不需要考虑各种转换问题。

Logo

旨在为数千万中国开发者提供一个无缝且高效的云端环境,以支持学习、使用和贡献开源项目。

更多推荐