介绍性实例
声明联合和声明结构非常类似。我们只需要将关键字“struct”替换为“union”。请考虑以下示例代码:
union test {
uint8_t c;
uint32_t i;
};
这指定了一个具有两个成员的模板:“c”(占用一个字节)和“i”(占用四个字节)。现在,我们可以创建此联合模板的变量:
union test u1;
使用成员运算符(.),我们可以访问“u1”联合的成员。例如,以下代码将10分配给上述联合的第二个成员,并将“c”的值复制到“m”变量(必须是uint8_t类型)。
u1.i=10;
m=u1.c;
将分配多少内存空间来存储“u1”变量?虽然结构的大小至少与其成员大小的总和一样大,但联合的大小等于其最大变量的大小。分配给联合的内存空间将在所有联合成员之间共享。在上面的例子中,“u1”的大小等于uint32_t的大小,即四个字节。该存储空间在“i”和“c”之间共享。因此,为这两个成员之一分配值将更改另一个成员的值。
需要共享内存空间吗?
让我们看一个联合可以成为有用数据对象的例子。假设,如下面的图1所示,系统中有两个设备需要相互通信。
图1
“设备A”应将状态,速度和位置信息发送到“设备B”。状态信息由三个变量组成,用于指示电池电量、工作模式和环境温度。该位置由两个显示x轴和y轴位置的变量表示。最后,速度由单个变量表示。假设这些变量的大小如下表所示。
如果“设备B”经常需要包含这些信息的每一部分,我们可以将所有这些变量存储在一个结构中并将结构发送到“设备B”。结构大小至少与这些变量大小的总和一样大,即9个字节。因此,每次“设备A”与“设备B”对话时,它需要通过两个设备之间的通信链路传输9字节数据帧。图2描绘了“设备A”用于存储变量和需要通过通信链路的数据帧的结构。
图2
但是,让我们考虑一个不同的场景,我们偶尔只需要发送状态信息。另外,假设在给定时间不需要同时获得位置和速度信息。换句话说,有时我们只发送位置,有时我们只发送速度,有时我们只发送状态信息。在这种情况下,将信息存储在九字节结构中并通过通信链路传输似乎不是一个好主意。
状态信息只能用三个字节表示; 对于位置和速度,我们分别只需要四个和两个字节。因此,“设备A”在一次传输中需要发送的最大字节数是4,因此,我们只需要4个字节的内存来存储该信息。这个四字节的存储空间将在我们的三种消息类型之间共享(参见图3)。另外,请注意,通过通信链路传输的数据帧的长度从9个字节减少到4个字节。
图3
总而言之,如果我们的程序具有互斥的变量,我们可以将它们存储在共享的内存区域中,以保留内存空间。这可能很重要,尤其是在内存受限的嵌入式系统环境中。在这种情况下,我们可以使用联合来创建所需的共享内存空间。上面的例子表明,使用联合来处理互斥变量也可以帮助我们节省通信带宽。节省通信带宽有时甚至比节省内存更重要。
对消息包使用联合
让我们看看我们如何使用联合来存储上面例子的变量。我们有三种不同的消息类型:状态、位置和速度。我们可以为状态和位置消息的变量创建一个结构(以便将这些消息的变量分组并作为单个数据对象进行操作)。以下结构用于此目的:
struct {
uint8_t power;
unit8_t op_mode;
uint8_t temp;
} status;struct {
uint16_t x_pos;
unit16_t y_pos;
} position;
我们可以将这些结构与“vel”变量一起放在一个联合中:
union {struct {
uint8_t power;
unit8_t op_mode;
uint8_t temp;
} status;struct {
uint16_t x_pos;
unit16_t y_pos;
} position;
uint16_t vel;
} msg_union;
上面的代码指定了一个联合模板,并创建了该模板的一个变量(名为“msg_union”)。在这个联合内部,有两个结构(“状态”和“位置”)和一个双字节变量(“vel”)。此并集的大小将等于其最大成员的大小,即“位置”结构,它占用四个字节的内存。该“存储空间”在“status”、“position”和“vel”变量之间共享。
如何跟踪联合活跃成员
我们可以使用上面联合的共享内存空间来存储我们的变量;但是,还有一个问题:接收器应该如何确定发送了哪种类型的消息?接收器需要识别消息类型以能够成功地解释所接收的信息。例如,如果我们发送“位置”消息,则接收数据的所有四个字节都很重要,但对于“velocity”消息,只应使用两个接收字节。要解决这个问题,我们需要将联合与另一个变量相关联,比如“msg_type”,它指示消息类型(或最后写入的联合成员)。与表示联合的活跃成员的离散值配对的联合称为“区分联合”或“标记联合”。
关于“msg_type”变量的数据类型,我们可以使用C语言的枚举数据类型来创建符号常量。但是,我们将使用一个字符来指定消息类型,只是为了使事情尽可能简单:
struct {
uint8_t msg_type;union {struct {
uint8_t power;
unit8_t op_mode;
uint8_t temp;
} status;struct {
uint16_t x_pos;
unit16_t y_pos;
} position;
uint16_t vel;
} msg_union;
} message;
我们可以考虑“msg_type”变量的三个可能值:“s”表示“状态”消息,“p”表示“位置”消息,“v”表示“速度”消息。我们可以将“消息”结构发送到“设备B”,并使用“msg_type”变量的值作为消息类型的指示符。例如,如果接收到的“msg_type”的值为“p”,则“设备B”将知道共享存储空间包含两个2字节变量。请注意,我们必须向通过通信链路发送的数据帧添加另一个字节,因为我们需要传输“msg_type”变量。另请注意,使用此解决方案,接收器不需要提前知道正在发送什么类型的消息。
另一种解决方案
我们看到联合允许我们声明共享内存区域以节省内存空间和通信带宽。但是,存在另一种存储互斥变量的方法,例如上例中的变量。第二种解决方案使用动态内存分配来存储每种消息类型的变量。同样,我们需要一个变量“msg_type”来指定通信链路的发送器和接收器端的消息类型。例如,如果“设备A”需要发送位置消息,它会将“msg_type”设置为“p”并分配四个字节的内存空间来存储“x_pos”和“y_pos”变量。接收器将检查“msg_type”的值,并根据其值创建适当的存储空间,用于存储和解释传入的数据帧。
使用动态内存在内存使用方面可以更有效,因为我们为每种消息类型分配了足够的空间。基于联合的解决方案并非如此。在那里,我们有四个字节的共享内存来存储所有三种消息类型,尽管“状态”和“速度”消息分别只需要三个和两个字节。但是,动态内存分配可能较慢,程序员需要包含释放分配内存的代码。这就是程序员通常喜欢使用基于联合的解决方案的原因。
联合的应用
似乎联合的最初目的是为互斥变量创建共享内存区域。但是,联合也被广泛用于从较大的数据对象中提取较小的数据部分。