21

我最近在当地图书馆的一次图书特卖会上看到了一本很棒的数据结构书,“ Data Structures Using C ”(c)1991,仅售 2 美元。正如书名所暗示的,本书涵盖了使用 C 编程语言的数据结构。

我知道这本书会过时,但可能包含许多我在其他地方不会遇到的高级 C 主题。

果然在 5 分钟内我发现了一些我不知道的关于 C 的东西。我遇到了一个讨论union关键字的部分,我意识到我从未使用过它,也从未见过任何代码。我很感激能学到一些有趣的东西,很快就买了这本书。

对于那些不了解工会是什么的人,本书使用了一个很好的比喻来解释:

要完全理解联合的概念,有必要检查它的实现。结构可以被视为通往内存区域的路线图。它定义了如何解释内存。一个联合为同一内存区域提供了几个不同的路线图,程序员有责任确定当前使用哪个路线图。在实践中,编译器会分配足够的存储空间来包含最大的联合成员。然而,路线图决定了如何解释存储。

我可以很容易地想出我会使用联合的人为情况或技巧。(但我对人为的情况或黑客不感兴趣......)

您是否使用或看到过使用 Union 解决问题的实现**比不使用 Union 更优雅**?

如果您快速解释为什么使用联合比不使用联合更好/更容易,则会获得额外奖励。

4

12 回答 12

26

UNION 在非 OOP 世界中实现了某种多态性。通常,您有一个通用部分,并且根据该部分,您可以使用其余的 UNION。因此,在您没有 OOP 语言并且希望避免过多的指针运算的情况下,联合在某些情况下可能更优雅。

于 2009-05-13T13:51:08.010 回答
19

它对于在寄存器中设置位而不是移位/屏蔽操作很有用:

typedef union {
    unsigned int as_int; // Assume this is 32-bits
    struct {
        unsigned int unused1 : 4;
        unsigned int foo : 4;
        unsigned int bar : 6;
        unsigned int unused2 : 2;
        unsigned int baz : 3;
        unsigned int unused3 : 1;
        unsigned int quux : 12;
    } field;
} some_reg;

注意:打包发生的方式取决于机器。

some_reg reg;
reg.field.foo = 0xA;
reg.field.baz = 0x5;
write_some_register(some_address, reg.as_int);

我可能在某处吹了一些语法,我的 C 生锈了 :)

编辑:

顺便说一句,这也以相反的方式起作用:

reg.as_int = read_some_register(some_address);
if(reg.field.bar == BAR_ERROR1) { ...
于 2009-05-13T14:04:01.033 回答
10

事实上,当您编写诸如设备驱动程序之类的东西(struct您想要发送到可以有几种相似但不同格式的设备)并且您需要精确的内存安排时,它是一个很好的工具......

于 2009-05-13T13:50:01.647 回答
8

您应该知道,在 C++ 中它们并不是一个很好的解决方案,因为只有 POD(普通旧数据)类型可以放在联合中。如果您的类有一个构造函数、析构函数,并且包含具有构造函数和/或析构函数的类(以及大约一百万个其他陷阱),则它不能是联合的成员。

于 2009-05-13T14:00:17.980 回答
6

我想,Union 是在 C/C++ 中实现类 VARIANT 数据类型的最简单方法。

于 2009-05-13T13:51:50.943 回答
5

它通常用于数据传输协议的规范中,您希望避免在数据结构中浪费空间。它允许通过为多个互斥选项使用相同的空间来节省内存空间。

例如:

enum PacketType {Connect, Disconnect};
struct ConnectPacket {};
struct DisconnectPacket {};
struct Packet
{
    // ...
    // various common data
    // ...
    enum PacketType type;
    union
    {
        ConnectPacket connect;
        DisconnectPacket disconnect;
    } payload;
};

ConnectPacket 和 DisconnectPacket 结构占用相同的空间,但这没关系,因为一个数据包不能同时是两种类型。枚举值用于确定联合的哪个部分正在使用。使用联合使我们能够避免重复数据包结构的公共部分。

于 2009-05-13T14:10:07.137 回答
4

考虑访问大变量中的单个字节的情况:

UInt32 x;
x = 0x12345678;
int byte_3 = x & 0x000000FF;          // 0x78
int byte_2 = (x & 0x0000FF00) >> 8;   // 0x56
int byte_1 = (x & 0x00FF0000) >> 16;  // 0x34
int byte_0 = (x & 0xFF000000) >> 24;  // 0x12

使用联合可以更加优雅:

typedef union
{
    UInt32 value;  // 32 bits
    Byte byte[4];  // 4 * 8 bits
}
UInt32_Bytes;

UInt32_Bytes x;
x.value = 0x12345678;
int byte_3 = x.byte[3];  // 0x78
int byte_2 = x.byte[2];  // 0x56
int byte_1 = x.byte[1];  // 0x34
int byte_0 = x.byte[0];  // 0x12

使用联合意味着您不再需要使用位掩码和移位运算符来访问各个字节。它还使字节访问显式。

于 2009-05-13T13:59:35.003 回答
4

这是获取浮点数的 IEEE 位值的好方法(当然假设浮点数是您系统上的 IEEE)。任何涉及将 float* 转换为 int* 的操作都有可能违反严格的别名规则。这不仅仅是理论上的——高水平的优化实际上会破坏你的代码。

从技术上讲,union 并没有解决这个问题。在实践中,所有已知的编译器将 (a) 允许您写入联合的一个成员并回读另一个成员,以及 (b) 在执行写入后执行读取。GCC 至少能够将联合滚动到寄存器中,将整个事情变成无操作(假设浮点数一开始就存储在寄存器中)。

于 2009-05-13T14:23:50.547 回答
2

我们在很多代码中使用联合来进行网络数据包解析。

联合分配最大元素的大小。您将创建一个具有最大消息大小的缓冲区元素的联合,然后您可以轻松访问数据包中的值。

假设数据“c123456”在线到达,您需要解析和访问这些值:

  #include <iostream>
  using namespace std;

  struct msg
  {
     char header;
     union
     {
       char a[3];
       char b[2];
       char c[5];
       char d[6];
       char buf[10];
     } data;
  } msg;

  int main()
  {
    struct msg m;
    memcpy(&m, "c123456", sizeof("c123456"));

    cout << "m.header: " << m.header << endl;
    cout << "m.data.d: " << string(m.data.d,sizeof(m.data.d)) << endl;
    cout << "m.data.b: " << string(m.data.b,sizeof(m.data.b)) << endl;

    switch (m.header)
    {
     case 'a': cout << "a: " << string(m.data.a, sizeof(m.data.a)) << endl; break;
     case 'b': cout << "b: " << string(m.data.b, sizeof(m.data.b)) << endl; break;
     case 'c': cout << "c: " << string(m.data.c, sizeof(m.data.c)) << endl; break;
     default: break;
    }
  }

输出将如下所示:

m.header: c
m.data.d: 123456
m.data.b: 12
c: 12345
于 2009-05-13T15:16:21.337 回答
2

我知道这已经重复了,但我将发布一个代码示例,看看联合体在读取网络流量时如何增加优雅和效率:

#pragma packed(1)
struct header_t {
   uint16_t msg_id;
   uint16_t size;
};
struct command_t {
   uint8_t cmd;
};
struct position_t {
   uint32_t x;
   uint32_t y;
   uint32_t z;
};
// ... Rest of the messages in an IDS
struct message {
   header_t header;
   union {
      command_t command;
      position_t position;
   } body;
};
#pragma packed(0)
message read( int socket ) {
   message data;
   unsigned int readed = read( socket, &data, sizeof(header_t) );
   // error checks... readed bytes smaller than header size and such
   readed = read( socket, &(data.body), data.header.size ); 
   // error checks...
}

在上面的代码片段中,您可以就地执行消息读取,您不需要关心接收到的对象的具体类型。如果您不使用联合,您将需要读取标题,提取大小和类型,实例化适当类型的对象(在层次结构中或包含在变体类型中作为 boost::any/ boost::variant),并在新创建的空间上执行第二次读取。

我们广泛使用此解决方案来控制模拟器(一些公司不喜欢 DDS 或 HLA 等“新”技术,并且仍然依赖原始 UDP/TCP 数据用于他们的模拟器)。在网络层中,我们使用在将其馈送到应用程序层之前转换为内部数据结构(网络到主机转换、数据缩放......)的联合。如前所述,您必须始终小心填充。

于 2009-09-02T19:11:10.287 回答
1

我曾经以与markh44 的答案类似的方式将它用于一种粗略的数据多态性。我有几种不同类型的数据,我想可能会使用它们。我创建了所有这些类型的联合和一个包含联合的结构和定义要使用的类型的代码。


union
{
    data_type_1;
    data_type_2;
    data_type_3;
} data_union;

typedef struct _TAG_DATA_WRAPPED_
{
    data_union data;
    int data_type; //better an enum
} WRAPPED_DATA;

WRAPPED_DATA loads_of_data[1024];


要回答您关于为什么这是有利的问题:

这使您可以轻松地分配不同类型数据的列表或数组,并以编程方式管理它们的类型。最大的问题当然是存储空间,因为如果类型的存储大小非常不同,您可能会浪费大量空间。

于 2009-05-13T16:37:16.943 回答
0

我认为这是一个很好的例子:

        struct fieldsv4{
            unsigned int ip4 : 8;
            unsigned int ip3 : 8;
            unsigned int ip2 : 8;
            unsigned int ip1 : 8;
        };
        typedef union {
            unsigned int ip32; // Assume this is 32-bits
            struct fieldsv4 part;
        } ipv4;

        ipv4 dir1;
        struct fieldsv4 f1 = {1, 1, 168, 192}; //for little endian depending OS  for big endian do not invert
        dir1.part = f1;
        ipv4 dir2= dir1;
        dir2.part.ip4 = 2;
        printf("%d.%d.%d.%d\n", dir2.part.ip1, dir2.part.ip2, dir2.part.ip3, dir2.part.ip4);
        printf("%d.%d.%d.%d\n", dir1.part.ip1, dir1.part.ip2, dir1.part.ip3, dir1.part.ip4);
        printf("%X\n", dir1.ip32 ^ dir2.ip32);
于 2020-02-14T17:03:43.543 回答