通过指针访问C联合成员

时间:2013-05-29 02:42:55

标签: c pointers unions

通过指针访问union成员(如下例所示)会导致C99中的未定义行为吗?意图似乎很清楚,但我知道在别名和联合方面存在一些限制。

union { int i; char c; } u;

int  *ip = &u.i;
char *ic = &u.c;

*ip = 0;
*ic = 'a';
printf("%c\n", u.c);

3 个答案:

答案 0 :(得分:15)

未指定(略微不同于undefined)行为,以通过上次写入的元素以外的任何元素访问union。这在C99附件J中有详细说明:

  

以下内容未指定:
  :
  除最后一个成员以外的工会成员的值(6.2.6.1)。

但是,由于您通过指针写入c,然后阅读c,此特定示例 已明确定义。 如何写入元素无关紧要:

u.c = 'a';        // direct write.
*(&(u.c)) = 'a';  // variation on yours, writing through element pointer.
(&u)->c = 'a';    // writing through structure pointer.

在评论中提出的一个问题似乎与此相矛盾,至少看似这样。用户davmac提供示例代码:

// Compile with "-O3 -std=c99" eg:
//  clang -O3 -std=c99 test.c
//  gcc -O3 -std=c99 test.c
// On clang v3.5.1, output is "123"
// On gcc 4.8.4, output is "1073741824"
//
// Different outputs, so either:
// * program invokes undefined behaviour; both compilers are correct OR
// * compiler vendors interpret standard differently OR
// * one compiler or the other has a bug

#include <stdio.h>

union u
{
    int i;
    float f;
};

int someFunc(union u * up, float *fp)
{
    up->i = 123;
    *fp = 2.0;     // does this set the union member?
    return up->i;  // then this should not return 123!
}

int main(int argc, char **argv)
{
    union u uobj;
    printf("%d\n", someFunc(&uobj, &uobj.f));
    return 0;
}

在不同的编译器上输出不同的值。但是,我认为这是因为它实际上违反了规则,因为它到成员f然后读取成员i,并且如附件J所示,这是未指明的。

6.5.2.3中的脚注82,其中指出:

  

如果用于访问union对象内容的成员与上次用于在对象中存储值的成员不同,则该值的对象表示的相应部分将被重新解释为对象表示形式。新型。

但是,由于这似乎违反了附件J的注释,并且它是处理x.y形式表达式的部分的脚注,它可能不适用于通过指针访问。

混叠应该严格的一个主要原因是允许编译器有更多的优化空间。为此,该标准要求将未写入类型的内存处理为未指定的类型。

举例来说,考虑提供的功能:

int someFunc(union u * up, float *fp)
{
    up->i = 123;
    *fp = 2.0;     // does this set the union member?
    return up->i;  // then this should not return 123!
}

实现可以自由地假设,因为你不是假设代替内存,up->i*fp是两个不同的对象。因此,您可以自由地假设在将up->i设置为123之后不会更改123,因此它可以简单地返回up->f = 2.0; ,而无需再次查看实际的变量内容。

相反,如果您将指针设置语句更改为:

union u up;
int x = someFunc (&u, &(up.f)); // <- aliasing here

然后这将使脚注82适用,返回的值将是浮点重新解释为整数。

我不认为这个问题的问题是因为你的写作然后阅读相同的类型,因此别名规则不起作用。


有趣的是,未指明的行为不是由函数本身引起的,而是通过调用它来引起的:

union u up;
float down;
int x = someFunc (&u, &down); // <- no aliasing

如果你是这样称呼它:

{{1}}

会成为问题。

答案 1 :(得分:4)

不,它不会,但你需要跟踪你输入联盟的最后一种类型。如果我要颠倒intchar作业的顺序,那将是一个非常不同的故事:

#include <stdio.h>

union { int i; char c; } u;

int main()
{
    int  *ip = &u.i;
    char *ic = &u.c;

    *ic = 'a';
    *ip = 123456;

    printf("%c\n", u.c); /* trying to print a char even though 
                            it's currently storing an int,
                            in this case it prints '@' on my machine */

    return 0;
}

编辑: 解释为什么它可能打印64('@')。

123456的二进制表示为0001 1110 0010 0100 0000。

对于64,它是0100 0000。

你可以看到前8位是相同的,并且由于printf被指示读取前8位,所以它只打印了那么多。

答案 2 :(得分:3)

它不是UB的唯一原因是因为你很幸运/不幸为其中一种类型选择char,而字符类型可以在C中为任何内容添加别名。例如,{{1}通过指针访问将是别名违规,因此是未定义的行为。}和int对于通过联合的直接访问,该行为被认为是缺陷报告283的解释的一部分:

http://www.open-std.org/jtc1/sc22/wg14/www/docs/dr_283.htm

当然,您仍然需要确保用于写入的类型的表示也可以解释为稍后用于读取的类型的有效(非陷阱)表示。