何时应该使用工会?为什么我们需要它们?
联合体常用于在整数和浮点数的二进制表示之间进行转换:
union
{
int i;
float f;
} u;
// Convert floating-point bits to integer:
u.f = 3.14159f;
printf("As integer: %08x\n", u.i);
虽然根据C标准来说这属于未定义行为(你只应该读取最近写入的字段),但在几乎任何编译器中它都会有明确定义的行为。
联合体有时也用于在C中实现伪多态性,方法是给一个结构体打上指示其包含的对象类型的标记,然后将可能的类型进行联合:
enum Type { INTS, FLOATS, DOUBLE };
struct S
{
Type s_type;
union
{
int s_ints[2];
float s_floats[2];
double s_double;
};
};
void do_something(struct S *s)
{
switch(s->s_type)
{
case INTS: // do something with s->s_ints
break;
case FLOATS: // do something with s->s_floats
break;
case DOUBLE: // do something with s->s_double
break;
}
}
这使得 struct S
的大小仅为 12 字节,而不是 28 字节。
联合体在嵌入式编程或需要直接访问硬件/内存的情况下特别有用。这里是一个简单的例子:
typedef union
{
struct {
unsigned char byte1;
unsigned char byte2;
unsigned char byte3;
unsigned char byte4;
} bytes;
unsigned int dword;
} HW_Register;
HW_Register reg;
然后你可以按以下方式访问reg:reg.dword = 0x12345678;
reg.bytes.byte3 = 4;
大小端(字节序)和处理器架构当然很重要。
另一个有用的功能是位修饰符:
typedef union
{
struct {
unsigned char b1:1;
unsigned char b2:1;
unsigned char b3:1;
unsigned char b4:1;
unsigned char reserved:4;
} bits;
unsigned char byte;
} HW_RegisterB;
HW_RegisterB reg;
使用这段代码,您可以直接访问寄存器/内存地址中的单个位:
x = reg.bits.b2;
b1
开始而不是b0
?问题在于没有关于顺序的信息。在你的例子中,b1
可以是位0或最高位(可能是位7)。 - 12431234123412341234123低级系统编程是一个合理的例子。
如果我没记错,我曾经使用联合体将硬件寄存器分解为组成位。这样,您就可以将8位寄存器(当时我做这个项目时如此)访问为组成位。
(我忘记了确切的语法,但是……)这个结构允许通过control_byte或单独的位访问控制寄存器。对于给定的字节序,确保位映射到正确的寄存器位非常重要。
typedef union {
unsigned char control_byte;
struct {
unsigned int nibble : 4;
unsigned int nmi : 1;
unsigned int enabled : 1;
unsigned int fired : 1;
unsigned int control : 1;
};
} ControlRegister;
我在几个库中看到这种方式被用作面向对象继承的替代方法。
例如:
Connection
/ | \
Network USB VirtualConnection
如果你希望将 Connection “类” 设定为上述的其中一个,你可以编写如下代码:
struct Connection
{
int type;
union
{
struct Network network;
struct USB usb;
struct Virtual virtual;
}
};
在 libinfinity 中的使用示例:http://git.0x539.de/?p=infinote.git;a=blob;f=libinfinity/common/inf-session.c;h=3e887f0d63bd754c6b5ec232948027cbbf4d61fc;hb=HEAD#l74
联合允许互斥的数据成员共享同一块内存。当内存较为稀缺,例如在嵌入式系统中,这一点非常重要。
在以下示例中:
union {
int a;
int b;
int c;
} myUnion;
这个联合体将占用一个单独的 int 空间,而不是三个单独的 int 值。如果用户设置了 a 的值,然后设置了 b 的值,它将覆盖 a 的值,因为它们都共享同一内存位置。
用途很多。只需执行grep union /usr/include/*
或类似目录中的命令。在大多数情况下,union
被包装在一个struct
中,并且结构体的一个成员告诉我们要访问的联合体元素。例如,查看man elf
来了解实际应用。
这是基本原则:
struct _mydata {
int which_one;
union _data {
int a;
float b;
char c;
} foo;
} bar;
switch (bar.which_one)
{
case INTEGER : /* access bar.foo.a;*/ break;
case FLOATING : /* access bar.foo.b;*/ break;
case CHARACTER: /* access bar.foo.c;*/ break;
}
这是我自己代码库中联合的一个例子(根据记忆和概括,可能不是完全准确)。它用于在我构建的解释器中存储语言元素。例如,下面的代码:
set a to b times 7.
由以下语言元素组成:
语言元素被定义为 '#define
' 值,如下所示:
#define ELEM_SYM_SET 0
#define ELEM_SYM_TO 1
#define ELEM_SYM_TIMES 2
#define ELEM_SYM_FULLSTOP 3
#define ELEM_VARIABLE 100
#define ELEM_CONSTANT 101
以下结构用于存储每个元素:
typedef struct {
int typ;
union {
char *str;
int val;
}
} tElem;
每个元素的大小都是最大联合的大小(对于类型和联合体,通常是4字节,但实际大小取决于实现)。
要创建一个“set”元素,您可以使用:
tElem e;
e.typ = ELEM_SYM_SET;
为了创建一个“variable[b]”元素,您可以使用:
tElem e;
e.typ = ELEM_VARIABLE;
e.str = strdup ("b"); // make sure you free this later
要创建一个“constant[7]”元素,您可以使用:
tElem e;
e.typ = ELEM_CONSTANT;
e.val = 7;
您可以轻松地将其扩展为包括浮点数(float flt
)或有理数(struct ratnl {int num; int denom;}
)和其他类型。
基本的前提是str
和val
在内存中不是连续的,它们实际上是重叠的,因此这是一种在同一块内存上获得不同视图的方法,如下所示,其中结构体基于内存位置0x1010
,整数和指针都占4个字节:
+-----------+
0x1010 | |
0x1011 | typ |
0x1012 | |
0x1013 | |
+-----+-----+
0x1014 | | |
0x1015 | str | val |
0x1016 | | |
0x1017 | | |
+-----+-----+
如果仅仅是结构,它看起来会像这样:
+-------+
0x1010 | |
0x1011 | typ |
0x1012 | |
0x1013 | |
+-------+
0x1014 | |
0x1015 | str |
0x1016 | |
0x1017 | |
+-------+
0x1018 | |
0x1019 | val |
0x101A | |
0x101B | |
+-------+
我认为它使得重复使用内存更加容易,即节省内存。例如,您想创建一个“变体”结构体,能够保存短字符串和数字:
struct variant {
int type;
double number;
char *string;
};
variant
实例至少需要使用96位或12字节。使用联合可以将大小缩小到64位或8字节。struct variant {
int type;
union {
double number;
char *string;
} value;
};
如果您想添加更多不同的变量类型等,可以通过使用union来节省更多内存。虽然使用void指针也可以实现类似的功能,但是union使得访问更加便捷并且类型安全。这样的节省听起来并不算太大,但是您可以将该结构体的所有实例所使用的内存减少三分之一。
很多答案都涉及将一种类型转换为另一种类型。当我解析串行数据流时,我从具有相同类型但更多的联合体中获得最大的用途。它们使得解析/构建帧格式数据包变得非常简单。
typedef union
{
UINT8 buffer[PACKET_SIZE]; // Where the packet size is large enough for
// the entire set of fields (including the payload)
struct
{
UINT8 size;
UINT8 cmd;
UINT8 payload[PAYLOAD_SIZE];
UINT8 crc;
} fields;
}PACKET_T;
// This should be called every time a new byte of data is ready
// and point to the packet's buffer:
// packet_builder(packet.buffer, new_data);
void packet_builder(UINT8* buffer, UINT8 data)
{
static UINT8 received_bytes = 0;
// All range checking etc removed for brevity
buffer[received_bytes] = data;
received_bytes++;
// Using the struc only way adds lots of logic that relates "byte 0" to size
// "byte 1" to cmd, etc...
}
void packet_handler(PACKET_T* packet)
{
// Process the fields in a readable manner
if(packet->fields.size > TOO_BIG)
{
// handle error...
}
if(packet->fields.cmd == CMD_X)
{
// do stuff..
}
}
编辑
有关大小端和结构体填充的评论是有效的,也是非常重要的问题。我几乎完全在嵌入式软件中使用这段代码,其中大部分我都能控制管道的两端。联合体(union)用于节省内存,特别是在内存有限的设备上,内存非常重要。
union _Union{
int a;
double b;
char c;
};
比如说,假设我们在内存有限的系统中需要使用上述三种数据类型(int,double,char)。如果我们不使用“联合”,我们需要分别定义这三种数据类型。在这种情况下,将分配 sizeof(a) + sizeof(b) + sizeof(c) 的内存空间。但是,如果我们使用联合,则只会根据这三种数据类型中最大的数据类型分配一个内存空间。因为联合结构中的所有变量都将使用相同的内存空间。因此,根据最大数据类型分配的内存空间将成为所有变量的共享空间。
union _Union{
int a;
double b;
char c;
};
int main() {
union _Union uni;
uni.a = 44;
uni.b = 144.5;
printf("a:%d\n",uni.a);
printf("b:%lf\n",uni.b);
return 0;
}
输出结果为: a: 0 b: 144.500000
为什么a的值是零呢?因为联合结构体只有一个内存区域,所有的数据结构体都共用这个内存。所以最后一次赋值会覆盖旧的数值。 再举一个例子:
union _Union{
char name[15];
int id;
};
int main(){
union _Union uni;
char choice;
printf("YOu can enter name or id value.");
printf("Do you want to enter the name(y or n):");
scanf("%c",&choice);
if(choice == 'Y' || choice == 'y'){
printf("Enter name:");
scanf("%s",uni.name);
printf("\nName:%s",uni.name);
}else{
printf("Enter Id:");
scanf("%d",&uni.id);
printf("\nId:%d",uni.id);
}
return 0;
}