为什么我们需要C联盟?


236

什么时候应该使用工会?我们为什么需要它们?

Answers:


252

联合通常用于整数和浮点数的二进制表示形式之间的转换:

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个字节。


应该有uy而不是uf
Amit Singh Tomar

1
假设将float转换为整数的示例有效吗?我不这么认为,因为int和float都以不同的格式存储在内存中。你能解释你的例子吗?
spin_eight 2012年

3
@spin_eight:不是从float到int的“转换”。更像是“重新解释浮点数的二进制表示,就好像它是一个int一样”。输出不是3:ideone.com/MKjwon 我不确定亚当为什么打印为十六进制。
endolith

@亚当·罗森菲尔德(Adam Rosenfield)我并没有真正理解转换,我在输出中没有得到整数:p
野兽

2
我认为应该删除关于未定义行为的免责声明。实际上,它是定义的行为。请参阅C99标准的脚注82:如果用于访问并集对象内容的成员与上次用于在对象中存储值的成员不同,则将对象表示该值的相应部分重新解释为一种新的对象表示形式,如6.2.6中所述(此过程有时称为“类型校正”)。这可能是陷阱表示。
Christian Gibbons

136

联合在嵌入式编程或需要直接访问硬件/内存的情况下特别有用。这是一个简单的示例:

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.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;

3
您在此处的答案与@Adam Rosenfield的上述答案共同构成了完美的互补对:您演示在联合中使用结构,而他演示在结构中使用联合。事实证明,我一次需要两个:在一个结构内的一个联合中的一个结构,以在嵌入式系统上的线程之间在C中实现一些奇特的消息传递多态性,如果我没有一起看过你的两个答案,我将不会意识到。
加布里埃尔·斯台普斯

1
我错了:这是一个结构内的一个联合,在一个结构内的一个联合中,从我最内层的嵌套到最外层的嵌套,就像我写的那样向左嵌套。我必须在最内层添加另一个并集,以允许使用不同数据类型的值。
加布里埃尔·斯台普斯

64

低级系统编程是一个合理的例子。

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;

3
这是一个很好的例子!这里是你如何使用嵌入式软件这种技术的例子:edn.com/design/integrated-circuit-design/4394915/...
rzetterberg

34

我已经在几个库中看到了它作为面向对象继承的替代品。

例如

        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


33

联合允许互斥的数据成员共享同一内存。当内存稀缺时(例如在嵌入式系统中),这非常重要。

在以下示例中:

union {
   int a;
   int b;
   int c;
} myUnion;

此联合将占用单个int的空间,而不是3个单独的int值。如果用户设置的值一个,然后设置的值b,它会覆盖的值,因为它们都共享相同的存储位置。


29

很多用法。只是做grep union /usr/include/*或在类似的目录中。在大多数情况下,union都用a包裹,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;
}

正是我想要的!替换某些省略号参数非常有用:)
Nicolas Voron 2013年

17

这是我自己的代码库中的联合示例(来自内存和措辞,因此可能不准确)。它用于将语言元素存储在我构建的解释器中。例如,以下代码:

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(float flt)或有理数(struct ratnl {int num; int denom;})和其他类型。

基本前提是,strand 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 |       |
       +-------+

是否make sure you free this later应从常量元素中删除注释?
Trevor

是的,@ Trevor,尽管我不敢相信您是最近4年以上第一个看到它的人:-)已修复,感谢您。
paxdiablo 2013年

7

我要说的是,它可以更轻松地重用可能以不同方式使用的内存,即节省内存。例如,您想执行一些“变体”结构,该结构可以保存短字符串和数字:

struct variant {
    int type;
    double number;
    char *string;
};

在32位系统中,这将导致至少有96位或12个字节用于以下情况的每个实例: variant

使用联合可以将大小减小到64位或8个字节:

struct variant {
    int type;
    union {
        double number;
        char *string;
    } value;
};

如果您想添加更多不同的变量类型等,则可以节省更多。的确,您可以执行类似的操作来铸造空指针-但是联合使它更易于访问,而且类型安全。这样的节省听起来并不庞大,但是您节省了用于该结构所有实例的三分之一的内存。


5

在特定情况下,当您需要这种灵活的结构时,可能很难想到,也许在消息协议中,您将发送不同大小的消息,但是即使那样,也可能会有更好,对程序员更友好的替代方法。

联合有点像其他语言中的变体类型-一次只能容纳一个东西,但是该东西可以是int,float等,具体取决于声明方式。

例如:

typedef union MyUnion MYUNION;
union MyUnion
{
   int MyInt;
   float MyFloat;
};

MyUnion将仅包含int或float,这取决于您最近设置的。这样做:

MYUNION u;
u.MyInt = 10;

您现在拥有一个等于10的整数;

u.MyFloat = 1.0;

您现在拥有等于1.0的浮点数。它不再拥有一个整数。显然,现在如果您尝试执行printf(“ MyInt =%d”,u.MyInt); 那么您可能会得到一个错误,尽管我不确定特定的行为。

联合的大小由其最大字段的大小(在本例中为float)决定。


1
sizeof(int) == sizeof(float)== 32)通常。
尼克T

1
对于记录,分配给浮点数然后打印int 不会导致错误,因为编译器和运行时环境都不知道哪个值有效。当然,对于大多数用途而言,打印出的int毫无意义。它将只是浮点数的内存表示形式,将其解释为一个int。
杰里B

4

当您要对由硬件,设备或网络协议定义的结构建模时,或者在创建大量对象并希望节省空间时,可以使用联合。不过,您确实确实有95%的时间不需要它们,而是坚持使用易于调试的代码。


4

这些答案中有许多涉及从一种类型转换为另一种类型。我从同类型的联合中获得最多的利用,而更多是同类型的(即,在解析串行数据流时)。它们允许解析/构造成帧的数据包变得微不足道。

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..
    }
}

编辑 有关字节序和结构填充的注释是有效的,也是非常重要的问题。我几乎完全在嵌入式软件中使用过这部分代码,其中大部分我可以控制管道的两端。


1
如果由于以下原因在两个不同的平台上交换数据,则此代码(大多数情况下)将不起作用:1)字节序可能不同。2)填充结构。
Mahori 2014年

@Ravi我同意有关字节顺序和填充的担忧。但是应该知道,我仅在嵌入式项目中使用过它。我大部分控制了管道的两端。
亚当·刘易斯

1

工会很棒。我见过的工会的一种巧妙用法是在定义事件时使用工会。例如,您可能决定一个事件为32位。

现在,在这32位中,您可能希望将前8位指定为事件发送者的标识符...有时您将事件作为一个整体来处理,有时您要对其进行剖析并比较其组成部分。工会使您可以灵活地同时执行这两个操作。

工会事件
{
  unsigned long eventCode;
  未签名的char eventParts [4];
};

1

关于什么VARIANT的COM接口使用呢?它有两个字段-“类型”和一个包含根据“类型”字段处理的实际值的并集。


1

在学校里,我像这样使用工会:

typedef union
{
  unsigned char color[4];
  int       new_color;
}       u_color;

我使用它来更轻松地处理颜色,而不是使用>>和<<运算符,而只需要遍历char数组的不同索引。


1

我在为嵌入式设备编码时使用了union。我有16位长的C int。当我需要从EEPROM读取/存储时,我需要检索高8位和低8位。所以我用这种方式:

union data {
    int data;
    struct {
        unsigned char higher;
        unsigned char lower;
    } parts;
};

它不需要移位,因此代码更易于阅读。

另一方面,我看到了一些旧的C ++ stl代码,这些代码使用union作为stl分配器。如果您有兴趣,可以阅读sgi stl源代码。这是其中的一部分:

union _Obj {
    union _Obj* _M_free_list_link;
    char _M_client_data[1];    /* The client sees this.        */
};

1
您是否不需要structhigher/ 周围进行分组lower?现在,两者都应仅指向第一个字节。
马里奥(Mario)

@Mario啊,对,我只是手写而忘了,谢谢
Mu Qiao


1

在早期的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];};

编译器本质上将产生一个结构大小(以及可能的对齐方式)表,以及一个单独的结构成员名称,类型和偏移量表。编译器不跟踪哪些成员属于其结构,并允许两个结构具有相同名称的成员只有在类型和偏移匹配(如成员qstruct xstruct y)。如果p是指向任何结构类型的指针,则p-> q会将“ q”的偏移量添加到指针p并从结果地址中获取“ int”。

给定以上语义,可以编写一个可以在多种结构上交替执行一些有用操作的函数,前提是该函数使用的所有字段都与所讨论结构中的有用字段对齐。这是一个有用的功能,将C更改为针对所讨论的结构类型来验证用于结构访问的成员将意味着在没有一种结构可以在同一地址包含多个命名字段的方法的情况下丢失它。在C语言中添加“联盟”类型有助于某种程度地弥补这一差距(尽管恕我直言,但它本来应该如此)。

工会填补这一空白的能力的重要组成部分是,可以将指向工会成员的指针转换为指向包含该成员的任何工会的指针,并且可以将指向任何工会的指针转换为针对任何成员的指针。尽管C89标准没有明确表示T*直接将a 强制转换为a U*等同于将其强制转换为指向同时包含T和的任何联合类型的指针U,然后将其强制转换为U*,但后一种强制转换序列的定义行为均不会受到使用的联合类型,并且标准没有为从T到直接的转换指定任何相反的语义U。此外,在函数接收到来源未知的指针的情况下,通过对象写入的行为T*将转换为T*到a U*,然后通过读取对象U*等同于通过type的成员编写并写T为type的联合U,这在某些情况下(例如,在访问Common Initial Sequence成员时)是标准定义的,而实现是定义的(而是比未定义)。尽管程序很少使用带有联合类型的实际对象的CIS保证,但是利用这样的事实是更为普遍的:指向未知来源的对象的指针的行为必须类似于指向联合成员的指针,并且具有与之关联的行为保证。


您能举一个这样的例子吗:“有可能编写一个可以在多种结构上交替执行某些有用操作的函数”。如何使用具有相同名称的多个结构成员?如果两个结构具有相同的数据对齐方式,因此一个成员具有与您的示例中相同的名称和相同的偏移量,那么我将从哪个结构中产生实际数据?(值)。两个结构具有相同的对齐方式和相同的成员,但其值不同。您能详细说明一下吗?
牧民

@Herdsman:在早期的C版本中,结构成员名称封装了类型和偏移量。当且仅当它们的类型和偏移量匹配时,不同结构的两个成员才可以具有相同的名称。如果struct member fooint偏移量为8的成员,则anyPointer->foo = 1234;意味着“在anyPointer中获取地址,将其移位8个字节,然后对所得地址执行值1234的整数存储。编译器无需知道或关心是否已anyPointer标识foo
超级猫

使用指针,您可以取消引用任何地址,而无论指针的“来源”是正确的,但是如果我可以获取数据,那么编译器保存结构成员及其名称表(如您在帖子中所说)的目的是什么?任何指针仅知道特定结构中成员的地址?如果编译器不知道该anyPointer标识符是否带有struct成员,那么编译器将如何检查to have a member with the same name only if the type and offset matched您的帖子的这些条件?
牧民

@Herdsman:编译器将保留结构成员名称的列表,因为的确切行为p->foo取决于的类型和偏移量foo。本质上p->foo是的简写*(typeOfFoo*)((unsigned char*)p + offsetOfFoo)。关于后一个问题,当编译器遇到结构成员定义时,它要求不存在具有该名称的成员,或者具有相同名称和类型的成员;我猜想如果存在不匹配的struct成员定义,那会很困扰,但是我不知道它是如何处理错误的。
supercat

0

一个简单而非常有用的例子是...

想像:

您有一个,uint32_t array[2]并且想要访问字节链的第3个和第4个字节。你可以做*((uint16_t*) &array[1])。但这可悲地打破了严格的别名规则!

但是已知的编译器允许您执行以下操作:

union un
{
    uint16_t array16[4];
    uint32_t array32[2];
}

从技术上讲,这仍然违反规则。但所有已知标准都支持此用法。

By using our site, you acknowledge that you have read and understand our Cookie Policy and Privacy Policy.
Licensed under cc by-sa 3.0 with attribution required.