6

从昨天对某个问题的回答中,我了解到写入一个联合成员并从另一个不同类型的成员读取值是不可移植且不安全的,假设成员的基本对齐。因此,经过一番研究,我发现了一个重复这一说法并指定了一个流行示例的书面来源 - 使用 int 和 float 的联合来查找 float 的二进制表示。

所以,理解这个假设是不安全的,我想知道 - 除了节省内存(呃......)工会有什么真正的用途?

注意:也就是说,在标准 C 下。显然,对于特定的实现,规则是预先知道的并且可以利用。

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

编辑2:由于这一点在答案中重复 -节省内存是一个有效的论点。我想知道除此之外是否还有什么。

4

10 回答 10

10

是的。

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

但是,其中一项功能在需要时才使用,而且很少需要。

于 2010-08-12T03:04:18.680 回答
4

即使unions 没有提供太多即时用途(除了减少内存使用),使用 aunion将其所有成员过度转储到 a 的一个优点struct是它使预期的语义清晰:只有一个值(或一组值,如果它是a unionof structs) 在任何给定时间都有效。它更好地记录了自己。

union如果您改为让所有成员成为 a的独立成员,则成员的互斥性将不那么明显struct。此外,如果您读取以前未写入的成员,您仍然会遇到同样的行为不明确的问题,但现在您还需要考虑应用程序的语义(是否将所有未使用的成员初始化为 0?它把它们当作垃圾?),所以从这个意义上说,你为什么使用工会?

于 2010-08-12T03:28:47.413 回答
3

这是联合的一种合法可移植使用:

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

再加上 中的类型信息tstruct arg可以便携地包含任何数字或指针值。整个结构的大小可能为 16-32 字节,而如果未使用联合则为 40-80 字节。如果我想分别保留每个可能的原始数字类型(signed char、short、int、long、long long、unsigned char、unsigned short ......)而不是将它们转换为最大的有符号/无符号/浮点类型,然后再存储它们。

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

于 2010-08-12T04:16:23.890 回答
3

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

于 2010-08-12T03:17:55.863 回答
3

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

您询问标准下的实际用法,但“实际用法”可能允许知识渊博的程序员以标准委员会不想预期或列举的方式利用实现定义的行为。我并不是说标准委员会考虑到了某种特定的行为,而是他们明确地希望将这种能力留在那里,以便以一种有用的方式加以利用。

换句话说:联合不一定对标准定义的行为有用,它们可以简单地在那里允许某人利用其目标机器的怪癖而无需诉诸汇编。

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

我希望这是有道理的。

于 2010-08-12T03:38:00.190 回答
3

即使不考虑对齐和打包已知的特定实现,联合仍然有用。

它们允许您将多个值之一存储到单个内存块中,如下所示:

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

是的,期望能够将数据存储onetwo. 但是,如果您简单地使用type来指定底层变量是什么,您可以轻松获得它而无需强制转换。

换句话说:

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

假设你type用来决定一个type1变量存储在那里是很好的。

于 2010-08-12T03:39:12.077 回答
2

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

假设你有一个结构是缓冲区

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

编辑:这是一个例子

struct X
{
  int a;
};

struct Y
{
  int b;
};

union Public
{
   struct X x;
   struct Y y;
};

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

所以给定一个函数:

void foo(Public* arg)
{   
...

您可以访问 struct X 或 struct Y

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

联合名称保持不变,但 struct 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;
...
于 2010-08-12T03:20:53.617 回答
2

使用 union 进行类型双关是不可移植的(尽管与任何其他类型双关方法相比并不特别不便携)。

例如,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

解析 MENU 给出了菜单定义;解析 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> font

澄清一下,该<font>部分是指生成的联合成员,第二个“字体”是指将产生该类型结果的解析器规则。这是此特定情况的规则:

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

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

当然,使用联合来节省内存已经不再那么重要了。虽然现在通常看起来相当微不足道,但当 64 Kb 的 RAM 很多时,内存节省意味着更多。

于 2010-08-12T03:45:49.407 回答
0

考虑具有不同位域的硬件控制寄存器。通过在寄存器的这些位域中设置值,我们可以控制寄存器的不同功能。

通过使用联合数据类型,我们可以修改寄存器的全部内容或寄存器的特定位域。

例如:考虑如下联合数据类型,

/* 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;

修改单个位域内容(对于 Ex Bitfield3 )

un_RegData.st_RegData.u32_BitField3 = 1;

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

于 2010-08-13T06:40:59.973 回答
0

这是一个实际的例子:

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

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

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

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

于 2018-08-26T15:35:53.343 回答