ANSI C联合 – 它们真的有用吗?

从昨天对某些问题的回答中,我了解到写入一个联盟成员并从另一个不同类型的成员中读取值是不可移植和不安全的,假设成员的基础对齐。 所以在经过一些研究之后,我发现了一个重复这个主张的书面来源并指定了一个流行的例子 – 使用int和float的并集来查找float的二进制表示。

因此,我想知道这个假设并不安全,我想 – 除了节省记忆(呃……)对工会有什么实际用途?

注意:即,在标准C下。显然,对于特定实现,规则是事先已知的并且可以被利用。

编辑:“不安全”这个词,由于近年来的联想,可能是一个不好的措辞选择,但我认为意图明确。

编辑2:由于这一点在答案中重复 – 节省内存是一个有效的参数。 我想知道是否还有其他的东西。

是。

提供了一种创建通用容器的方法。 但是,要获得多态行为,您必须实现vtable或类型切换自己…

但是,有一些function只在您需要时使用,而且很少需要。

即使union并没有立即提供有用的东西(减少内存使用量),使用union而不是将其所有成员转储到struct一个好处是它可以使目标语义清晰:只有一个值(或者一组)值,如果它是struct s的union )在任何给定时间都有效。 它更好地记录自己。

如果您将所有union成员分别作为struct成员,那么成员的互斥性将不那么明显。 此外,如果您读取以前未写入的成员,您仍然会遇到与定义不明确行为相同的问题,但现在您也需要考虑应用程序的语义(它是否将所有未使用的成员初始化为0?它会把它们留作垃圾吗?),所以从这个意义上说,你为什么使用联盟呢?

是的,工会可能是不可移植的,不安全但有其用途。 例如,它可以通过消除将uint32转换为char [4]的需要来加快速度。 如果您尝试通过SW中的IP地址进行路由,这可能会派上用场,但您的处理器端序必须是网络顺序。 将工会视为铸造的替代方案,减少机器指令。 铸造也有类似的缺点。

该问题包含一个可能不允许有效答案的约束……

您询问标准下的实际使用情况,但“实际使用”可能允许知识渊博的程序员以标准委员会不希望预期或枚举的方式利用实现定义的行为。 我并不是说标准委员会有一个特定的行为,但他们明确地希望将这种能力留在那里以有用的方式被利用。

换句话说:Unions 对于标准定义的行为通常不一定有用,它们可以简单地允许某人利用其目标机器的怪癖而无需求助于汇编。

可能有一百万种有用的方法在实现定义的方式中使用它们在各种机器上使用它们,并且以严格可移植的方式使用它们是零有用的方法,但是这些百万次实现定义的用法足以使它们的存在标准化。

我希望这是有道理的。

即使对已知对齐和打包的特定实现进行折扣,工会仍然有用。

它们允许您将多个值中的一个存储到单个内存块中,顺序如下:

 typedef struct { int type; union { type1 one; type2 two; } } unioned_type; 

是的,期望能够将您的数据存储到one并从two读取它不可移植的。 但是如果你只是使用type来指定底层变量是什么,那么你可以轻松地获得它而无需强制转换。

换一种说法:

 unioned_type ut; ut.type = 1; ut.one = myOne; // Don't use ut.two here unless you know the underlying details. 

假设您使用type来确定type1变量存储在那里,这很好。

这是一个合法的便携式工会用途:

 struct arg { enum type t; union { intmax_t i; uintmax_t u; long double f; void *p; void (*fp)(void); } v; }; 

结合t类型信息, struct arg可以包含任何数字或指针值。 整个结构的大小可能是16-32字节,而如果没有使用union,则为40-80字节。 如果我想分别保留每个可能的原始数字类型(signed char,short,int,long,long long,unsigned char,unsigned short,…)而不是将它们转换为最大的签名,那么差异将更加极端/ unsigned /浮点类型在存储之前。

此外,虽然它不是“可移植”来假设除unsigned char之外的类型的表示,但是标准允许使用带有unsigned char的联合或者将指针强制转换为unsigned char *并以这种方式访问​​任意数据对象。 如果将该信息写入磁盘,则无法将其移植到使用不同表示的其他系统,但它在运行时仍可能有用 – 例如,实现哈希表来存储double值。 (如果填充位问题使得这种技术无效,任何人都想纠正我吗?)如果没有别的,它可以用来实现memcpy (不是很有用,因为标准库为你提供了更好的实现)或者(更有趣的是) memswap函数它可以用有界临时空间交换两个任意大小的对象。 现在已经有了一些外部使用领域的联盟和unsigned char * cast领域,但它是密切相关的。

我遇到的一种使用联合的方法是数据隐藏。

假设您有一个缓冲区结构

然后通过允许在某些模块中的struct上联合,您可以以不同的方式访问缓冲区的内容,或者根本不访问该特定模块中声明的union。

编辑:这是一个例子

 struct X { int a; }; struct Y { int b; }; union Public { struct X x; struct Y y; }; 

这里使用union XY的人可以将XY转换为struct X或Y.

给定一个function:

 void foo(Public* arg) { ... 

你可以访问struct X或struct Y.

但是你想限制访问权限,以便用户不知道X.

联合名称保持不变但结构X部分不可用(通过标题)

 void foo(Public* arg) { // Public is still available but struct X is gone, // user can only cast to struct Y struct Y* p = (struct Y*)arg; ... 

使用用于类型双关语的联合是不可移植的(尽管不比任何其他类型的双关语方法更不便携)。

例如,OTOH是一个解析器,通常有一个联合来表示表达式中的值。 [编辑:我正在用一个替换解析器示例,我希望它更容易理解]:

我们来考虑一个Windows资源文件。 您可以使用它来定义菜单,对话框,图标等资源。如下所示:

 #define mn1 2 mn1 MENU { MENUITEM "File", -1, MENUBREAK } ico1 "junk.ico" dlg1 DIALOG 100, 0, 0, 100, 100 BEGIN FONT 14, "Times New Roman" CAPTION "Test Dialog Box" ICON ico1, 700, 20, 20, 20, 20 TEXT "This is a string", 100, 0, 0, 100, 10 LTEXT "This is another string", 200, 0, 10, 100, 10 RTEXT "Yet a third string", 300, 0, 20, 100, 10 LISTBOX 400, 20, 20, 100, 100 CHECKBOX "A combobox", 500, 100, 100, 200, 10 COMBOBOX 600, 100, 210, 200, 100 DEFPUSHBUTTON "OK", 75, 200, 200, 50, 15 END 

解析菜单会给出一个菜单定义; 解析DIALOG会给出对话框定义等等。 在解析器中,我们将其表示为联合:

 %union { struct control_def { char window_text[256]; int id; char *class; int x, y, width, height; int ctrl_style; } ctrl; struct menu_item_def { char text[256]; int identifier; } item; struct menu_def { int identiifer; struct menu_item_def items[256]; } mnu; struct font_def { int size; char filename[256]; } font; struct dialog_def { char caption[256]; int id; int x, y, width, height; int style; struct menu_def *mnu; struct control_def ctrls[256]; struct font_def font; } dlg; int value; char text[256]; }; 

然后我们通过解析特定类型的表达式来指定将生成的类型。 例如,文件中的字体定义将成为联合的font成员:

 %type  font 

只是为了澄清, 部分引用生成的union成员,第二个“font”引用将产生该类型结果的解析器规则。 以下是此特定案例的规则:

 font: T_FONT T_NUMBER "," T_STRING { $$.size = $2; strcpy($$.filename,$4); }; 

是的,理论上我们可以在这里使用结构而不是联合 – 但是除了浪费内存之外,它只是没有意义。 文件中的字体定义定义字体。 除了它实际定义的字体之外,让它生成包含菜单定义,图标定义,数字,字符串等的结构是没有意义的。 [编辑结束]

当然,使用工会来节省内存很少是非常重要的。 虽然现在通常看起来相当微不足道,但当64 Kb的RAM很多时,内存节省意味着更多。

考虑使用不同位字段的硬件控制寄存器。 通过设置寄存器的这些位字段中的值,我们可以控制寄存器的不同function。

通过使用Union Data类型,我们可以修改寄存器的整个内容或寄存器的特定位域。

对于Ex:考虑一个union数据类型如下,

 /* Data1 Bit Defintion */ typedef union { struct STRUCT_REG_DATA { unsigned int u32_BitField1 : 3; unsigned int u32_BitField2 : 2; unsigned int u32_BitField3 : 1; unsigned int u32_BitField4 : 2; } st_RegData; unsigned int u32_RegData; } UNION_REG_DATA; 

要修改注册表的整个内容,

 UNION_REG_DATA un_RegData; un_RegData. u32_RegData = 0x77; 

修改单个位字段内容(For Ex Bitfield3)

 un_RegData.st_RegData.u32_BitField3 = 1; 

两者都反映在同一记忆中。 然后可以将该值写入硬件控制寄存器的值。

这是一个实际的例子:

有些微控制器的非易失性存储器以字节块存储数据。 你怎么能轻松地在那些记忆中存储一系列花车? 我们知道在C浮点数是32位(4字节)长,所以:

 union float_uint8 { uint8 i[KNFLOATS*4]; //or KNFLOATS*sizeof(float) float f[KNFLOATS]; }; 

现在,您可以使用float_uint8类型的变量/指针存储/寻址浮点数并使用循环,您可以轻松地将它们作为分解字节存储在内存中,而无需进行任何转换或分解。 阅读记忆时,同样的故事也在重复。 即使您不需要知道如何以字节为单位分解浮点数来存储或恢复存储在内存中的数据。

这个例子是从我自己的作品中提取的。 所以是的,它们很有用。