【问题标题】:ANSI C unions - are they really useful?ANSI C 联合——它们真的有用吗?
【发布时间】:2011-03-28 17:24:53
【问题描述】:

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

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

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

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

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

【问题讨论】:

  • 请链接到引用的问题和书面来源
  • 我实际上不记得问题本身(这不是那里的主题,而是在其中一个 cmets 上提出的)。来源是"C - A reference Manual", 5th Ed., Harbison/Steele
  • 是这个吗? stackoverflow.com/questions/3443751/… 如果是这样,请在相同的答案下查看我的 cmets。
  • 未定义的行为和实现定义的行为之间存在很大差异。两者都不可移植,但后者在您知道实现细节时非常有用。
  • 我也不同意您帖子中的“不安全”声明。确实不便携,但可以轻松安全可靠地使用。

标签: c unions


【解决方案1】:

是的。

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

,但是,这些功能之一是您仅在需要时才使用并且很少需要时使用。

【讨论】:

  • 好的,我想我明白你的意思了。但是,既然需要你自己实现多态机制,那么与声明不同类型的变量并通过机制切换的指针访问它们(除了节省内存)相比,它有什么优势?
  • 另外,这是标准编写者的意图,还是后来开发的智能用例?
  • 节省内存(写c时“大”机器的特性值得思考:共享机器上的主内存以千字为单位),简化复杂结构的管理,少一个字符在每次访问时输入,有能力以任何你想要的方式使用内存,并且不要忽视执行那些“未定义”事情的能力,因为并非每个程序都需要可移植。跨度>
  • 我确信 K&R 只是想到了智能汇编编码器所做的事情,即在计数时将每个字节挤出。
【解决方案2】:

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

【讨论】:

  • 正如我在问题中提到的,显然联合对于特定实现非常有用。您正在假设对底层存储的特定假设。我的问题是它们在 standard C 角度下是否有用。 dmckee 对此给出了合理的论据。
【解决方案3】:

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

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

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

编辑:这是一个例子

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

联合名称保持不变,但结构 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;
...

【讨论】:

  • ::blink:: 这很聪明,但是......它也是通过默默无闻的安全性,所以可能不是一个好主意。对打包问题也很敏感,因此依赖于实现。
  • 我不确定我是否明白你的意思。能否举例说明这种缓冲区的定义?
  • @dmckee - 你同意我的怀疑,这个用例是特定于实现的!
  • @dmckee,正确的封装不是“通过默默无闻的安全”。显然,恶意调用者甚至可以在 C 中执行类似 *(char *)rand() = rand(); 的操作,因此隐藏您的实现对安全没有好处。另一方面,隐藏实现细节确实会强烈阻止使用您的代码的其他人在您稍后调整实现时会破坏内部结构。
【解决方案4】:

即使unions 没有提供太多即时用途(除了减少内存使用),使用union 而不是将其所有成员转储到struct 的一个优点是它产生了预期的语义明确:在任何给定时间,只有一个值(或一组值,如果它是 unionstructs)是有效的。它可以更好地记录自己。

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

【讨论】:

  • 我认为这只是部分正确 - 我的理解是同一类型的成员保证相互覆盖,所以这里没有互斥体。
  • @ysap:确实如此,但这是与我的观点无关的联合实施细节。
  • @jamesdin - 你的意思是“由于成员的未知安排,确实假设访问排他性”(或者不是吗?)。我的评论是,这只是部分正确。
  • @ysap:我的观点是,struct 本身并没有传达关于其成员应该如何相对于彼此使用的语义信息。我没有说“未知安排”。
【解决方案5】:

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

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

换句话说:联合不一定要对标准定义的行为有用才能在一般情况下有用,它们可以简单地允许某人利用其目标机器的怪癖而不诉诸于组装。

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

我希望这是有道理的。

【讨论】:

  • 是的,确实有道理,谢谢。我目前不完全确定我是否同意回答我的问题。
  • @ysap - 不是真的,但我认为这更像是一个答案而不是评论:)
【解决方案6】:

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

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

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 变量存储在那里是很好的。

【讨论】:

  • 如果我理解你的例子,它实际上和我上面提到的参考书中给出的例子很相似。然后,争论归结为节省内存
  • +1 — 这可能是在标准范围内使用联合的最有用的方式。
  • 好的,你更新之后,我相信它实际上和那个例子很相似。
  • 是的,在早期,节省内存是一个非常重要的考虑因素。 C 有时被描述为一种具有汇编程序的所有功能和...语言。在随后的每个标准(ANSI 和 ISO)中,他们都非常小心,不会不必要地破坏现有代码。
  • 这不仅仅是“早期”的考虑——它在嵌入式设备上很重要,这仍然是 C 的大量用例。甚至嵌入式开发人员也可能希望在某个时候升级或更改他们的编译器.
【解决方案7】:

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

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

澄清一下,&lt;font&gt; 部分是指生成的联合成员,第二个“字体”是指将生成该类型结果的解析器规则。以下是针对这种特殊情况的规则:

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

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

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

【讨论】:

  • 对不起,杰瑞,我不明白你的例子。您能否举一个具体的例子说明您定义的联合与它应该代表的表达式之间的关系?
  • @ysap:我很害怕——我会尝试写一个 small 语法来展示它的工作原理。
  • 感谢您澄清用例。我想我理解了这个信息。您基本上将联合用作通用容器。然后,对象的 type 实际占用并存储在 tag 中(我假设它是valuetext 成员/s)。因此,与其他答案中的其他示例一样,这基本上是多态性的显式实现。
  • 实际上不,它不使用标签——但解析器生成器会跟踪您提供的&lt;type&gt;标签,如果您尝试混淆它会出错成员(例如,由于font 规则产生font 结果,它只能分配给联合的font 成员)。是的,我想您可以将其视为与多态性类似,但并非完全如此。
【解决方案8】:

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

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 字节,而如果未使用联合则为 40-80 字节。如果我想分别保留每个可能的原始数字类型(signed char、short、int、long、long long、unsigned char、unsigned short ......)而不是将它们转换为最大的有符号/unsigned/floating point type 在存储它们之前。

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

【讨论】:

  • R,你答案的前半部分很清楚——节省内存。我不确定我理解的是第二部分。如何使用 unsigned char 成员以可预测的方式访问其他成员?
  • 在不知道实现的情况下,这些值不是“可预测的”,但它们是实现定义的。只要您的代码不对这些值进行假设,而只是在内部使用它们,就可以了。当您不关心排序与原始类型的自然数字排序有任何关系时,另一个可能的应用程序是创建一个逐字节比较函数,以便与 qsort 一起使用,只要它定义明确并且结果是可重复的。
【解决方案9】:

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

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

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

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

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

【讨论】:

  • @barati21 - 这正是问题的重点 - 如果您想保证可移植性,您不应该这样做。内存中联合成员的底层布局没有在标准中定义。它是实现定义的
【解决方案10】:

这是一个实际的例子:

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

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

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

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

【讨论】:

    猜你喜欢
    • 2016-10-25
    • 1970-01-01
    • 2012-12-17
    • 2010-09-10
    • 1970-01-01
    • 1970-01-01
    • 2020-02-19
    • 2014-03-30
    • 1970-01-01
    相关资源
    最近更新 更多