为什么我们需要C联盟?
什么时候应该使用工会? 为什么我们需要他们?
联盟通常用于在整数和浮点数的二进制表示之间进行转换:
union { int i; float f; } u; // Convert floating-point bits to integer: uf = 3.14159f; printf("As integer: %08x\n", ui);
虽然在技术上这是根据C标准的未定义的行为(你只能读最近写的字段),但它几乎在任何编译器中都会以一种明确定义的方式行事。
联盟有时也被用来实现C中的伪多态,通过给结构一些标签指明它包含的对象types,然后将可能的types联合在一起:
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个。
embedded式编程或需要直接访问硬件/存储器的情况下,联合会特别有用。 这是一个微不足道的例子:
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;
字节顺序(字节顺序)和处理器体系结构当然很重要。
另一个有用的function是位修饰符:
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;
我已经在几个库中看到它作为面向对象inheritance的替代品。
例如
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
联盟允许互斥的数据成员共享相同的内存。 当内存更为稀缺时,如embedded式系统,这一点非常重要。
在以下示例中:
union { int a; int b; int c; } myUnion;
这个联合会占用一个int的空间,而不是3个独立的int值。 如果用户设置a的值,然后设置b的值,则会覆盖a的值,因为它们共享相同的存储位置。
很多用法。 只要grep union /usr/include/*
或类似的目录。 大多数情况下, union
包装在一个struct
, 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; }
下面是我自己的代码库(从内存和释义所以它可能不是确切的)的联合的例子。 它被用来在我build立的解释器中存储语言元素。 例如,下面的代码:
set a to b times 7.
由以下语言元素组成:
- 符号[组]
- variables并[a]
- 符号[到]
- variables并[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;
那么每个元素的大小是最大联合的大小(types为4个字节,联合为4个字节,尽pipe这些是典型值, 实际大小取决于实现)。
为了创build一个“设置”元素,你可以使用:
tElem e; e.typ = ELEM_SYM_SET;
为了创build一个“variables[b]”元素,你可以使用:
tElem e; e.typ = ELEM_VARIABLE; e.str = strdup ("b"); // make sure you free this later
为了创build一个“常量[7]”元素,你可以使用:
tElem e; e.typ = ELEM_CONSTANT; e.val = 7;
你可以很容易地将其扩展为包含浮点数( float flt
)或有理数( struct ratnl {int num; int denom;}
)和其他types。
基本的前提是, 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 | | +-------+
我想说,这可以更容易地重用可能以不同方式使用的内存,即节省内存。 例如,你想要做一些能够保存一个简短string以及一个数字的“variant”结构:
struct variant { int type; double number; char *string; };
在32位系统中,这将导致每个variant
实例使用至less96位或12个字节。
使用联合可以将大小减小到64位或8个字节:
struct variant { int type; union { double number; char *string; } value; };
如果你想添加更多不同的variablestypes等等,你可以节省更多。这也许是事实,你可以做类似的事情来铸造一个空指针 – 但是联合使得它更容易访问以及types安全。 这样的节省听起来不是很大,但是你节省了用于这个结构的所有实例的内存的三分之一。
当你需要这种灵活的结构时,很难想象一个特定的场合,也许在一个消息协议中,你将发送不同大小的消息,但即使如此,也许有更好的和更多的程序员友好的select。
工会有点像其他语言的变体types – 他们一次只能容纳一件事,但是这个东西可能是一个int,一个浮点数等,这取决于你如何声明它。
例如:
typedef union MyUnion MYUNION; union MyUnion { int MyInt; float MyFloat; };
MyUnion将只包含一个int或一个浮点数, 取决于你最近设置的 。 所以这样做:
MYUNION u; u.MyInt = 10;
你现在保持一个int等于10;
u.MyFloat = 1.0;
你现在拥有一个等于1.0的浮动。 它不再拥有一个整数。 显然现在如果你尝试做printf(“MyInt =%d”,u.MyInt); 那么你可能会得到一个错误,虽然我不确定的具体行为。
工会的规模取决于其最大领域的大小,在这种情况下是浮动。
当您想要build模由硬件,设备或networking协议定义的结构时,或者当您创build大量对象并希望节省空间时使用联合。 95%的时间确实不需要它们,坚持易于debugging的代码。
这些答案中的很多都涉及从一种types转换到另一种types。 我从使用相同types的工会(即parsing串行数据stream时)获得了最多的用途。 它们允许parsing/构build分组的分组变得微不足道。
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.. } }
编辑有关endianness和结构填充的评论是有效的,很好,关注。 我已经使用这个代码几乎完全在embedded式软件,其中大部分我已经控制pipe道的两端。
那么在COM接口中使用VARIANT
呢? 它有两个字段 – “types”和一个联合持有根据“types”字段处理的实际值。
在学校里,我用这样的工会:
typedef union { unsigned char color[4]; int new_color; } u_color;
我用它来处理颜色更容易,而不是使用>>和<<运算符,我只需要通过我的字符数组的不同索引。
我在编写embedded式设备时使用了union。 我有C int是16位长。 当我需要从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. */ };
- 包含不同loggingtypes的文件。
- 包含不同请求types的networking接口。
看看这个: X.25缓冲区命令处理
许多可能的X.25命令之一被接收到一个缓冲区中,并通过使用所有可能的结构的UNION来处理。
在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];};
一个编译器本质上会产生一个结构体大小(可能alignment)的表,以及一个单独的结构体成员名,types和偏移量的表。 编译器没有跟踪哪些成员属于哪个结构,并且只有在匹配的types和偏移量(如struct x
和struct y
成员q
,才允许两个结构体具有相同名称的成员。 如果p是指向任何结构types的指针,则p-> q会将“q”的偏移量添加到指针p,并从结果地址中获取“int”。
考虑到上述语义,可以编写一个函数,它可以交替地对多种结构执行一些有用的操作,前提是函数使用的所有字段都与所讨论的结构中的有用字段alignment。 这是一个有用的function,并且改变C以validation用于针对所讨论的结构的types的结构访问的成员将意味着在缺less可以在相同地址处包含多个命名字段的结构的方式的情况下将其丢失。 向C中添加“union”types有助于弥补这一差距(虽然不是,恕我直言,以及它应该是)。
工会填补这一空缺的一个重要组成部分是这样一个事实,即指向一个工会成员的指针可以被转换成指向任何包含该成员的工会的指针,而指向任何工会的指针可以被转换为指向任何成员的指针。 虽然C89标准并没有明确地说将一个T*
直接转换成一个U*
就相当于将它转换为指向包含T
和U
任何联合types的指针,然后将其转换为U*
,但是没有定义的后面的转换序列会受到使用的联合types的影响,标准没有指定从T
到U
的直接转换的任何相反的语义。 此外,在函数接收到未知来源的指针的情况下,通过T*
写入对象的行为,将T*
转换为U*
,然后通过U*
读取对象等同于通过成员书写联合types为T
,读取types为U
,在less数情况下(例如访问通用初始序列成员时)将标准定义,其余部分则为实现定义的(而不是未定义的)。 虽然很less有程序利用联盟types的实际对象来开展独联体保证,但利用指向不明来源的对象的指针必须像工会成员的指针那样行事,并具有与之相关联的行为保证的事实,更为常见。
工会是伟大的。 我见过的一个聪明的使用工会是定义一个事件时使用它们。 例如,你可能决定一个事件是32位。
现在,在这32位内,您可能希望指定前8位作为事件发送者的标识符。有时,您将整个事件作为一个整体进行处理,有时候会对其进行parsing并比较其组件。 工会给你灵活性做两个。
联合事件 { unsigned long eventCode; unsigned char eventParts [4]; };
一个简单而有用的例子是…
想像:
你有一个uint32_t array[2]
并想访问字节链的第三和第四字节。 你可以做*((uint16_t*) &array[1])
。 但这可悲的是打破了严格的锯齿规则!
但已知的编译器允许您执行以下操作:
union un { uint16_t array16[4]; uint32_t array32[2]; }
从技术上讲,这仍然是违反规定的。 但所有已知的标准都支持这种用法。