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

308

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


21个回答

4

很难想象在什么情况下需要这种类型的灵活结构,也许在消息协议中,您将发送不同大小的消息,但即使在那种情况下,可能还有更好、更适合程序员的替代方案。

联合体有点像其他语言中的变体类型 - 它们一次只能容纳一件事情,但这件事情可以是 int、float 等,具体取决于您如何声明它。

例如:

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

MyUnion只会包含int或float,取决于你最近设置的是哪个。所以进行以下操作:

MYUNION u;
u.MyInt = 10;

现在,您持有一个等于10的整数(int);

u.MyFloat = 1.0;

现在,你拥有一个等于1.0的浮点数。它不再是整数。显然,如果你尝试执行printf("MyInt=%d", u.MyInt);,那么你可能会遇到错误,虽然我不确定具体的行为。

联合体的大小由其最大字段的大小决定,在本例中为浮点数。


1
sizeof(int) == sizeof(float)== 32)通常是成立的。 - Nick T
2
就记录而言,先将值分配给浮点数,然后打印整数不会导致错误,因为编译器和运行时环境都不知道哪个值是有效的。当然,打印的整数对于大多数用途来说毫无意义。它只是浮点数的内存表示,解释为整数。 - Jerry B

3

在学校里,我使用联合体(unions)如下:

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

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


3

当你想要模拟由硬件、设备或网络协议定义的结构,或者创建大量对象并希望节省空间时,可以使用联合。但实际上,在95%的情况下,你并不需要它们,最好使用易于调试的代码。


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

编译器会生成一个结构大小的表格(可能包括对齐方式),以及一个结构成员名称、类型和偏移量的单独表格。编译器不会跟踪哪些成员属于哪个结构,并且只有在类型和偏移量匹配时才允许两个结构具有相同名称的成员(例如,结构x和结构y的成员q)。如果p是指向任何结构类型的指针,则p->q将把“q”的偏移量添加到指针p并从结果地址中提取一个“int”。
基于上述语义,可以编写一个函数,可对多种结构执行一些有用的操作,前提是该函数使用的所有字段与所涉及的结构中的有用字段相对应。这是一个有用的功能,如果更改C语言以验证用于结构访问的成员是否与所涉及的结构类型相匹配,那么在没有一种能够在同一地址处包含多个命名字段的结构的情况下,将失去它。添加"union"类型有助于在某种程度上填补这个空缺(尽管我认为它的效果不如应该)。
联合的能力填补这个空缺的一个重要部分是,联合成员的指针可以转换为包含该成员的任何联合的指针,并且可以将任何联合的指针转换为任何成员的指针。虽然C89标准没有明确表示将T*直接转换为U*等同于将其转换为包含T和U的任何联合类型的指针,然后将其转换为U*,但是后者的转换序列的任何定义行为都不会受到使用的联合类型的影响,并且标准没有为直接从T到U的转换规定任何相反的语义。此外,在函数接收来自未知来源的指针的情况下,通过T*写入对象、将T*转换为U*,然后通过U*读取对象的行为将等效于通过类型为T的成员编写联合并按类型为U读取,这在某些情况下是标准定义的(例如访问公共初始序列成员),对于其他情况则是实现定义的(而不是未定义的)。尽管很少有程序利用联合类型的实际对象来利用CIS保证,但利用来自未知来源的对象的指针必须像指向联合成员的指针一样运行,并具有与之相关的行为保证则更为常见。

你能给出这个例子吗:“可以编写一个函数,以可互换的方式执行多种结构上的一些有用操作”。如何使用相同名称的多个结构成员?如果两个结构具有相同的数据对齐方式,并且有一个与您的示例中相同偏移量的相同名称成员,那么我将从哪个结构中获得实际数据(值)?两个结构具有相同的对齐方式和相同的成员,但它们上面的值不同。请详细说明。 - Herdsman
@Herdsman:在早期版本的C语言中,结构体成员名称封装了类型和偏移量。只有当它们的类型和偏移量匹配时,两个不同结构体的成员才能具有相同的名称。如果结构体成员“foo”是一个偏移量为8的“int”,那么“anyPointer->foo = 1234;”的意思是“取出anyPointer中的地址,将其偏移8个字节,并对结果地址执行整数存储,存储值为1234”。编译器不需要知道或关心“anyPointer”是否标识了任何具有“foo”列在其成员中的结构体类型。 - supercat
通过指针,您可以取消引用任何地址,而不考虑指针的“起源”,这是正确的。但是,如果我只知道特定结构中成员的地址,那么编译器为什么要保存结构成员及其名称的表(如您在帖子中所说)?如果编译器不知道anyPointer是否与结构成员相对应,那么编译器如何检查您帖子中的“仅当类型和偏移量匹配时才具有相同名称的成员”的条件呢? - Herdsman
@Herdsman:编译器会保留结构体成员名单,因为p->foo的精确行为取决于foo的类型和偏移量。从本质上讲,p->foo*(typeOfFoo*)((unsigned char*)p + offsetOfFoo)的简写形式。至于你后面的问题,当编译器遇到一个结构体成员定义时,它要求不存在具有该名称的成员,或者该名称的成员具有相同的类型和偏移量;如果存在不匹配的结构体成员定义,我猜想编译器会报错,但我不知道它如何处理错误。 - supercat

1

关于在COM接口中使用的VARIANT,它有两个字段 - "type"和一个联合体,其中包含一个实际值,该值根据"type"字段进行处理。


1

联合体很棒。我见过联合体的一个巧妙用法是在定义事件时使用它们。例如,您可能决定一个事件是32位。

现在,在这32位中,您可能想将前8位指定为事件发送者的标识符...有时您会将整个事件处理,有时您会分解它并比较其组件。联合体使您能够灵活地执行这两种操作。

union Event
{
  unsigned long eventCode;
  unsigned char eventParts[4];
};

0

在我编写嵌入式设备的代码时,我使用了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.        */
};

1
你不需要在 higher/ lower 周围加上一个分组的 struct 吗?现在两个都应该只指向第一个字节。 - Mario
@Mario 哦,对了,我刚刚手写了它,然后就忘了,谢谢。 - Mu Qiao

0

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

想象一下:

你有一个uint32_t array[2],想要访问字节链的第三个和第四个字节。 你可以这样做:*((uint16_t*) &array[1])。 但很遗憾,这违反了严格别名规则!

但已知的编译器允许你做以下操作:

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

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


0
  • 一个包含不同记录类型的文件。
  • 一个包含不同请求类型的网络接口。

看一下这个:X.25缓冲区命令处理

其中一个可能的X.25命令被接收到缓冲区中,并通过使用所有可能结构的联合在原地处理。


请问您能否解释一下这两个例子吗?我的意思是它们与联合体有什么关系。 - Amit Singh Tomar

0
我发现使用union可以方便地确定最大常量的大小。
#include <stdio.h>
#include <time.h>

#define MON_1 "January"
#define MON_2 "February"
#define MON_3 "March"
#define MON_4 "April"
#define MON_5 "May"
#define MON_6 "June"
#define MON_7 "July"
#define MON_8 "August"
#define MON_9 "September"
#define MON_10 "October"
#define MON_11 "November"
#define MON_12 "December"
#define WDAY_1 "Sunday"
#define WDAY_2 "Monday"
#define WDAY_3 "Tuesday"
#define WDAY_4 "Wednesday"
#define WDAY_5 "Thursday"
#define WDAY_6 "Friday"
#define WDAY_7 "Saturday"

int main(void)
{
  time_t t = time(NULL);
  struct tm *t_tm = gmtime(&t);
  char *t_wday[] = {
    WDAY_1,
    WDAY_2,
    WDAY_3,
    WDAY_4,
    WDAY_5,
    WDAY_6,
    WDAY_7
  },
  *t_mon[] = {
    MON_1,
    MON_2,
    MON_3,
    MON_4,
    MON_5,
    MON_6,
    MON_7,
    MON_8,
    MON_9,
    MON_10,
    MON_11,
    MON_12
  },
  t_arr[sizeof(union {
    char mon[sizeof MON_1],
    mon2[sizeof MON_2],
    mon3[sizeof MON_3],
    mon4[sizeof MON_4],
    mon5[sizeof MON_5],
    mon6[sizeof MON_6],
    mon7[sizeof MON_7],
    mon8[sizeof MON_8],
    mon9[sizeof MON_9],
    mon10[sizeof MON_10],
    mon11[sizeof MON_11],
    mon12[sizeof MON_12];
  }) + sizeof(union {
    char wday[sizeof WDAY_1],
    wday2[sizeof WDAY_2],
    wday3[sizeof WDAY_3],
    wday4[sizeof WDAY_4],
    wday5[sizeof WDAY_5],
    wday6[sizeof WDAY_6],
    wday7[sizeof WDAY_7];
  }) + sizeof "00 00:00:00 0000"];

  sprintf(t_arr, "%s %s %d %d:%d:%d %d", t_wday[t_tm->tm_wday], t_mon[t_tm->tm_mon], t_tm->tm_mday, t_tm->tm_hour, t_tm->tm_min, t_tm->tm_sec, t_tm->tm_year + 1900);
  printf("%s\n", t_arr);
}

这样可以避免手动定义不正确的数组大小的风险。

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