为什么我们需要C联合体?

308

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


21个回答

309

联合体常用于在整数和浮点数的二进制表示之间进行转换:

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


1
例子中的将浮点数转换为整数的示例有效吗?我认为不是,因为 int 和 float 在内存中以不同的格式存储。您能解释一下您的示例吗? - spin_eight
7
@spin_eight说的不是将浮点数转换为整数,而更像是“以整数的方式重新解释浮点数的二进制表示”。输出结果不是3:http://ideone.com/MKjwon。但我不确定Adam为什么要打印十六进制。 - endolith
@Adam Rosenfield 我并没有真正理解转换,输出中我没有得到一个整数 :p - The Beast
@Krunal,您的计算机大小不同的原因是,联合的大小取决于其成员中最大类型的大小。-> 您的平台报告的大小与OP平台报告的大小(16 vs 12)之间的差异是由于类型具有__最小__大小,但在各种不同的架构中可以超过这些大小限制。 - datUser
10
我认为应该删除关于未定义行为的免责声明。实际上,这是有定义的行为。请参考C99标准的脚注82:如果用于访问联合对象内容的成员与最后用于在对象中存储值的成员不同,则将值的对象表示的适当部分重新解释为新类型中的对象表示,如6.2.6所述。这个过程有时被称为“类型游走”。 - Christian Gibbons
显示剩余5条评论

183

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

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;

9
你的回答与@Adam Rosenfield上面的答案结合起来,使得二者组成了完美的互补对:你展示了如何在联合体中使用结构体,而他则展示了如何在结构体中使用联合体。结果发现我需要同时使用两者:在嵌入式系统的C语言线程之间实现一些花哨的消息传递多态性,需要一个结构体内嵌于联合体内部的结构体。如果没有看到你们俩个的答案,我就不会意识到这一点。 - Gabriel Staples
1
我错了:它是一个联合体,位于一个结构体内部,再位于另一个联合体内部,最后位于最外层的结构体内,从我写下的内嵌最深层次到最外层。我不得不在最内层添加另一个联合体,以允许不同数据类型的值。 - Gabriel Staples
2
为什么你从b1开始而不是b0?问题在于没有关于顺序的信息。在你的例子中,b1可以是位0或最高位(可能是位7)。 - 12431234123412341234123
1
@GabrielStaples,你能否提供一个例子并将其作为答案发布吗?我只是好奇。 - Unknown123

72

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

如果我没记错,我曾经使用联合体将硬件寄存器分解为组成位。这样,您就可以将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;

4
这是一个绝佳的例子!下面有一个使用此技术在嵌入式软件中的示例:http://www.edn.com/design/integrated-circuit-design/4394915/Managing-the-8--to-32-bit-processor-migration - rzetterberg

42

我在几个库中看到这种方式被用作面向对象继承的替代方法。

例如:

        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


上面的链接已经失效了:/ 不过这个例子很有趣! - gordon_freeman

40

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

在以下示例中:

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

这个联合体将占用一个单独的 int 空间,而不是三个单独的 int 值。如果用户设置了 a 的值,然后设置了 b 的值,它将覆盖 a 的值,因为它们都共享同一内存位置。


37

用途很多。只需执行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;
}

正是我所需要的!非常有用的替代一些省略参数 :) - Nicolas Voron

21

这是我自己代码库中联合的一个例子(根据记忆和概括,可能不是完全准确)。它用于在我构建的解释器中存储语言元素。例如,下面的代码:

set a to b times 7.

由以下语言元素组成:

  • 符号 [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;})和其他类型。

基本的前提是strval在内存中不是连续的,它们实际上是重叠的,因此这是一种在同一块内存上获得不同视图的方法,如下所示,其中结构体基于内存位置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 |       |
       +-------+

常量元素中的“确保稍后释放此内容”注释应该被删除吗? - Trevor
1
是的,@Trevor,虽然我不敢相信在过去的4年中你是第一个看到它的人 :-) 已经修复了,谢谢。 - paxdiablo

9

我认为它使得重复使用内存更加容易,即节省内存。例如,您想创建一个“变体”结构体,能够保存短字符串和数字:

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

在32位系统中,每个variant实例至少需要使用96位或12字节。使用联合可以将大小缩小到64位或8字节。
struct variant {
    int type;
    union {
        double number;
        char *string;
    } value;
};

如果您想添加更多不同的变量类型等,可以通过使用union来节省更多内存。虽然使用void指针也可以实现类似的功能,但是union使得访问更加便捷并且类型安全。这样的节省听起来并不算太大,但是您可以将该结构体的所有实例所使用的内存减少三分之一。


6

很多答案都涉及将一种类型转换为另一种类型。当我解析串行数据流时,我从具有相同类型但更多的联合体中获得最大的用途。它们使得解析/构建帧格式数据包变得非常简单。

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)结构中的填充。 - mahoriR
@Ravi 我同意对字节序和填充的担忧。但是需要知道的是,我在嵌入式项目中专门使用了这个。其中大部分我都控制了管道的两端。 - Adam Lewis

4

联合体(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;
}

注意:Union的大小是其最大字段的大小,因为必须保留足够的字节来存储最大的字段。

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接