如下例所示,通过指针访问联合成员是否会导致 C99 中的未定义行为?意图似乎很清楚,但我知道关于别名和联合有一些限制。
union { int i; char c; } u;
int *ip = &u.i;
char *ic = &u.c;
*ip = 0;
*ic = 'a';
printf("%c\n", u.c);
通过除最后写入的元素之外的任何元素访问联合是未指定的(与未定义的细微不同)行为。这在 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 所示,这是未指定的。
有一个脚注 82,6.5.2.3
其中指出:
如果用于访问联合对象内容的成员与上次用于在对象中存储值的成员不同,则该值的对象表示的适当部分将被重新解释为新类型中的对象表示。
但是,由于这似乎违反了附件 J 的评论,并且它是处理 form 表达式的部分的脚注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;
那么这将使脚注 82 适用,并且返回的值将是浮点数的重新解释为整数。
我认为这不是问题的原因是因为您的写作然后阅读相同的类型,因此别名规则不起作用。
有趣的是,未指定的行为不是由函数本身引起的,而是由调用它引起的:
union u up;
int x = someFunc (&u, &(up.f)); // <- aliasing here
如果你改为这样称呼它:
union u up;
float down;
int x = someFunc (&u, &down); // <- no aliasing
这不会是一个问题。
不,它不会,但您需要跟踪您放入联合的最后一个类型是什么。如果我要颠倒你int
和char
作业的顺序,那将是一个非常不同的故事:
#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 位,因此它只打印同样多的内容。
它不是 UB 的唯一原因是因为您足够幸运/不幸地选择char
了其中一种类型,并且字符类型可以在 C 中为任何内容设置别名。例如,如果类型是int
and float
,则通过指针进行的访问将是别名违规和因此未定义的行为。对于通过联合直接访问,该行为被认为是缺陷报告 283 解释的一部分:
http://www.open-std.org/jtc1/sc22/wg14/www/docs/dr_283.htm
当然,您仍然需要确保用于写入的类型的表示也可以解释为稍后用于读取的类型的有效(非陷阱)表示。