有两种(或至少是在C90中)使这种不确定行为成为可能。首先是允许编译器生成额外的代码,该代码跟踪联合中的内容,并在您访问错误的成员时生成信号。实际上,我认为没有人做过(也许是CenterLine吗?)。另一个是优化的可能性,并加以利用。我使用过的编译器会将写操作推迟到最后一个可能的时刻,原因是可能没有必要(因为变量超出范围,或者随后写入了另一个值)。从逻辑上讲,人们希望在看到联合时可以关闭此优化,但是在最早的Microsoft C版本中却没有。
类型修饰的问题很复杂。C委员会(在1980年代后期)或多或少地采取了这样的立场:您应该使用强制类型转换(在C ++中,reinterpret_cast),而不是使用联合,尽管这两种技术在当时都很普遍。从那时起,一些编译器(例如g ++)采取了相反的观点,支持联合的使用,但不支持强制转换的使用。而且在实践中,如果不能立即明显发现类型绑定,那么它们都不起作用。这可能是g ++观点背后的动机。如果访问工会成员,则很明显可能存在类型绑定。但是,当然,给出如下所示:
int f(const int* pi, double* pd)
{
int results = *pi;
*pd = 3.14159;
return results;
}
致电:
union U { int i; double d; };
U u;
u.i = 1;
std::cout << f( &u.i, &u.d );
根据标准的严格规则是完全合法的,但对于g ++(可能还有许多其他编译器)而言,它是失败的;编译时f
,编译器假定pi
,并pd
不能别名,并重新排序的写入*pd
和读取*pi
。(我相信永远不会保证这一点。但是标准的当前措辞确实可以保证这一点。)
编辑:
由于其他答案认为该行为实际上是已定义的(很大程度上是基于引用非规范性注释,出于上下文):
正确的答案是pablo1977:标准不尝试定义涉及类型punning的行为。可能的原因是,它没有可定义的可移植行为。这不会阻止特定的实现定义它;尽管我不记得有关该问题的任何具体讨论,但是我很确定这样做的目的是实现可以定义一些东西(大多数(如果不是全部的话)都可以做到)。
关于使用联合进行类型处理:在C委员会开发C90时(在1980年代后期),明确的意图是允许调试实现进行附加检查(例如,使用胖指针进行边界检查)。从当时的讨论中可以明显看出,调试的实现可能会缓存有关在联合中初始化的最后一个值的信息,并在尝试访问其他任何内容时进行陷阱。在第6.7.2.1/16节中明确指出:“最多可以随时将一个成员的值存储在联合对象中。” 访问不存在的值存在未定义的行为;它可以被同化为访问未初始化的变量。(当时有一些关于访问具有相同类型的其他成员是否合法的讨论。我不知道最终的决议是什么。在1990年左右之后,我开始使用C ++。)
关于C89的引用,它的行为是实现定义的:在第3节(术语,定义和符号)中找到它似乎很奇怪。我必须在家中的C90副本中查找它;事实上,它已在更高版本的标准中删除,这表明委员会认为它的存在是错误的。
标准支持的并集的使用是模拟派生的一种方式。您可以定义:
struct NodeBase
{
enum NodeType type;
};
struct InnerNode
{
enum NodeType type;
NodeBase* left;
NodeBase* right;
};
struct ConstantNode
{
enum NodeType type;
double value;
};
union Node
{
struct NodeBase base;
struct InnerNode inner;
struct ConstantNode constant;
};
并合法访问base.type,即使Node是通过初始化的inner
。(第6.5.2.3/6节以“做出了一项特别保证……”并继续明确允许这一事实的事实,非常有力地表明,所有其他情况都是未定义的行为。当然,是指“在本国际标准中否则以未定义的行为来表示未定义的行为”或第4/2节中省略了对行为的任何明确定义的陈述;目的是说明该行为不是未定义的,则必须显示它在标准中的定义位置。)
最后,关于类型修剪:所有(或至少我使用过的所有)实现都以某种方式支持它。当时我的印象是,意图是指针转换是实现支持它的方式。在C ++标准中,甚至有(非规范性的)文字表明reinterpret_cast
熟悉基础架构的人“不足为奇”。但是,实际上,只要访问是通过并集成员,大多数实现都支持将并集用于类型修剪。大多数实现(但不支持g ++)也支持指针强制转换,前提是编译器可以清楚看到指针强制转换(对于某些未指定的指针强制转换定义)。基础硬件的“标准化”意味着:
int
getExponent( double d )
{
return ((*(uint64_t*)(&d) >> 52) & 0x7FF) + 1023;
}
实际上很便携。(当然,它不适用于大型机。)不起作用的是类似我的第一个示例的示例,在该示例中,编译器看不到别名。(我很确定这是标准中的缺陷。我似乎还记得有关于它的灾难恢复。)