什么时候应该使用工会?为什么我们需要它们?
20 回答
联合通常用于在整数和浮点数的二进制表示之间进行转换:
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;
低级系统编程就是一个合理的例子。
IIRC,我使用联合将硬件寄存器分解为组件位。因此,您可以访问组件位中的 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
如果您希望连接“类”是上述任何一种,您可以编写如下内容:
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 的空间,而不是 3 个单独的 int 值。如果用户设置a的值,然后设置b的值,它将覆盖 a 的值,因为它们都共享相同的内存位置。
很多用法。只需执行grep union /usr/include/*
或在类似目录中。大多数情况下,它union
被包裹在 astruct
中,并且结构的一个成员告诉联合中的哪个元素要访问。例如签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.
由以下语言元素组成:
- 符号[集]
- 变量[a]
- 符号[到]
- 变量[b]
- 符号[次]
- 常数[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 字节,联合为 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;
};
在 32 位系统中,这将导致至少 96 位或 12 个字节用于variant
.
使用联合,您可以将大小减小到 64 位或 8 字节:
struct variant {
int type;
union {
double number;
char *string;
} value;
};
如果您想添加更多不同的变量类型等,您可以节省更多。这可能是真的,您可以做类似的事情来转换一个 void 指针 - 但联合使它更容易访问以及类型安全的。这样的节省听起来并不大,但是您节省了用于该结构的所有实例的内存的三分之一。
很难想到需要这种灵活结构的特定场合,也许是在一个消息协议中,您将发送不同大小的消息,但即便如此,也可能有更好、对程序员更友好的替代方案。
联合有点像其他语言中的变体类型——它们一次只能容纳一个东西,但那个东西可能是 int、float 等,这取决于你如何声明它。
例如:
typedef union MyUnion MYUNION;
union MyUnion
{
int MyInt;
float MyFloat;
};
MyUnion 将仅包含一个 int 或一个 float,具体取决于您最近设置的 . 所以这样做:
MYUNION u;
u.MyInt = 10;
u 现在拥有一个等于 10 的 int;
u.MyFloat = 1.0;
u 现在持有一个等于 1.0 的浮点数。它不再拥有一个 int。显然,现在如果您尝试执行 printf("MyInt=%d", u.MyInt); 那么你可能会得到一个错误,虽然我不确定具体的行为。
联合的大小由其最大字段的大小决定,在本例中为浮点数。
其中许多答案涉及从一种类型转换为另一种类型。我从具有相同类型的联合中获得最多的使用(即在解析串行数据流时)。它们使框架数据包的解析/构造变得微不足道。
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..
}
}
编辑 关于字节序和结构填充的评论是有效的,而且非常关注。我几乎完全在嵌入式软件中使用了这段代码,其中大部分我都控制了管道的两端。
当您想要对由硬件、设备或网络协议定义的结构进行建模时,或者当您创建大量对象并想要节省空间时,可以使用联合。不过,你真的 95% 的时间都不需要它们,坚持使用易于调试的代码。
在学校,我使用这样的工会:
typedef union
{
unsigned char color[4];
int new_color;
} u_color;
我用它来更轻松地处理颜色,而不是使用 >> 和 << 运算符,我只需要遍历我的 char 数组的不同索引。
在 C 的早期版本中,所有结构声明都将共享一组公共字段。鉴于:
struct x {int x_mode; int q; float x_f};
struct y {int y_mode; int q; int y_l};
struct z {int z_mode; char name[20];};
编译器基本上会生成一个结构大小(可能还有对齐)表,以及一个单独的结构成员名称、类型和偏移量表。编译器不跟踪哪些成员属于哪些结构,并且仅当类型和偏移匹配时才允许两个结构具有同名的成员(如and的q
成员)。如果 p 是指向任何结构类型的指针,则 p->q 会将“q”的偏移量添加到指针 p 并从结果地址中获取“int”。struct x
struct y
鉴于上述语义,可以编写一个函数,该函数可以互换地对多种结构执行一些有用的操作,前提是该函数使用的所有字段都与相关结构中的有用字段对齐。这是一个有用的特性,并且更改 C 以验证用于针对所讨论的结构类型访问结构的成员将意味着在没有一种方法可以在同一地址包含多个命名字段的结构的情况下丢失它。向 C 添加“联合”类型有助于在一定程度上填补这一空白(尽管不是,恕我直言,它应该是)。
联合体填补这一空白的一个重要部分是指向联合体成员的指针可以转换为指向包含该成员的任何联合体的指针,并且指向任何联合体的指针可以转换为指向任何成员的指针。虽然 C89 标准没有明确说明将 aT*
直接转换为 aU*
等同于将其转换为指向任何同时包含T
and的联合类型的指针U
,然后将其转换为U*
,但后一个转换序列的已定义行为不会受到使用了联合类型,并且标准没有为从T
to的直接转换指定任何相反的语义U
。此外,在函数接收到未知来源的指针的情况下,通过 写入对象的行为T*
,转换T*
到 a U*
,然后通过读取对象U*
相当于通过 type 的成员写入联合T
并读取为 type U
,这在少数情况下是标准定义的(例如,当访问公共初始序列成员时)和实现定义的(而不是其余部分比未定义)。虽然很少有程序利用联合类型的实际对象利用 CIS 保证,但利用指向未知来源对象的指针必须表现得像指向联合成员的指针并具有与之相关联的行为保证这一事实要普遍得多。
工会很棒。我见过的一种巧妙的联合使用方法是在定义事件时使用它们。例如,您可能决定一个事件是 32 位的。
现在,在这 32 位中,您可能希望将前 8 位指定为事件发送者的标识符……有时您将事件作为一个整体来处理,有时您将其分解并比较它的组成部分。工会让你可以灵活地做这两件事。
工会活动 { 无符号长事件代码; 无符号字符事件部分[4]; };
我在为嵌入式设备编码时使用了 union。我有 16 位长的 C int 。当我需要读取/存储到 EEPROM 时,我需要检索高 8 位和低 8 位。所以我用了这种方式:
union data {
int data;
struct {
unsigned char higher;
unsigned char lower;
} parts;
};
它不需要移位,因此代码更易于阅读。
另一方面,我看到一些旧的 C++ stl 代码将联合用于 stl 分配器。有兴趣的可以阅读sgi stl源码。这是其中的一部分:
union _Obj {
union _Obj* _M_free_list_link;
char _M_client_data[1]; /* The client sees this. */
};
VARIANT
那在 COM 接口中使用呢?它有两个字段 - “type”和一个持有实际值的联合,该值根据“type”字段进行处理。
union 用于节省内存,特别是在内存很重要的内存有限的设备上使用。经验:
union _Union{
int a;
double b;
char c;
};
例如,假设我们在内存有限的系统中需要上述3种数据类型(int,double,char)。如果我们不使用“union”,我们需要定义这3种数据类型。在这种情况下会分配sizeof(a) + sizeof(b) + sizeof(c) 内存空间。但是如果我们使用onion,则只会根据这3种数据类型中最大的数据类型分配一个内存空间。因为联合结构中的所有变量都将使用相同的内存空间。因此,根据最大数据类型分配的内存空间将是所有变量的公共空间。例如:
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;
}
注意:并集的大小是其最大字段的大小,因为必须保留足够数量的字节来存储较大的字段。
一个简单且非常有用的例子是......
想象:
您有一个uint32_t array[2]
并且想要访问字节链的第 3 和第 4 个字节。你可以做*((uint16_t*) &array[1])
。但这可悲地违反了严格的别名规则!
但是已知的编译器允许您执行以下操作:
union un
{
uint16_t array16[4];
uint32_t array32[2];
}
从技术上讲,这仍然违反了规则。但是所有已知的标准都支持这种用法。
当您有一些函数返回的值可能因函数所做的不同而不同时,请使用联合。