标准定义了什么类型的双关语/指针魔法?

时间:2014-02-12 01:49:41

标签: c pointers undefined language-lawyer type-punning

我似乎无法绕过C标准的某些部分,所以我来到这里是为了清除那种模糊的,焦虑的不确定性,当我不得不考虑这些技巧是什么定义的行为和什么未定义或违反标准。我不在乎它是否会起作用,我关心C标准是否认为它是合法的,定义的行为。

比如这,我相当肯定是UB:

struct One
{
        int Hurr;
        char Durr[2];
        float Nrrr;
} One;

struct Two
{
        int Hurr;
        char Durr[2];
        float Nrrr;
        double Wibble;
} Two;

One = *(struct One*)&Two;

这不是我所说的全部。比如将指针转换为One to int *,并取消引用它等等。我希望能够很好地理解这些内容的定义,以便我可以在晚上睡觉。如果可以,请引用标准,但请务必指定它是C89还是C99。 C11太新了,无法信任这样的问题恕我直言。

3 个答案:

答案 0 :(得分:1)

我认为从技术上讲,这个例子也是UB。但它几乎肯定会起作用,gcc和clang都没有用-pedantic抱怨它。

首先,以下内容在C99(§6.5.2.3/ 6)中有明确定义:[1]

union OneTwo {
  struct One one;
  struct Two two;
};

OneTwo tmp = {.two = {3, {'a', 'b'}, 3.14f, 3.14159} };
One one = tmp.one;

访问“受到惩罚的”struct Oneunion必须有效这一事实意味着struct Two前缀的布局与struct One相同。这不能取决于union的存在,因为给定的复合类型只能有一个存储布局,并且其布局不能取决于它在union中的使用,因为union不需要对使用struct的每个翻译单位都可见。

此外,在C中,所有类型都不超过一个字节序列(例如,C++)(§6.2.6.1/ 4)[2]。因此,以下也可以保证起作用:

struct One one;
struct Two two = ...;
unsigned char tmp[sizeof one];
memcpy(tmp, two, sizeof one);
memcpy(one, tmp, sizeof one);

鉴于以上内容以及任何指针类型对void*的可转换性,我认为可以合理地断定上面的临时存储是不必要的,并且可以直接写为:

struct One one;
struct Two two = ...;
unsigned char tmp[sizeof one];
memcpy(one, two, sizeof one);

从那里到通过别名指针直接赋值,就像OP一样,这不是一个很大的飞跃,但是别名指针还有一个问题:理论上,指针转换可能会创建一个无效的指针,因为struct Two*的位格式可能与struct One*不同。虽然将一个指针类型转换为具有更宽松对齐(§6.3.2.3/ 7)[3]的另一个指针类型然后再将其转换回来是合法的,但不能保证转换后的指针实际可用,除非转换是到一个字符类型。特别是,struct Two的对齐可能与struct One的对齐不同(更严格),并且更强对齐的指针的位格式不能直接用作指向不太强烈对齐的结构的指针。但是,很难看到反对几乎相当的论点:

one = *(struct One*)(void*)&two;

虽然标准可能无法明确保证这一点。

在评论中,不同的人提出了混淆优化的幽灵。上面的讨论根本没有触及别名,因为我认为它与简单的赋值无关。必须在任何前面的表达式之后和任何后续表达式之前对赋值进行排序;它明确地修改了one,几乎同样明确地引用了two。一个优化使得two的先前法律变异对于赋值是不可见的,将是非常可疑的。

但通常情况下,别名优化是可能的。因此,即使所有上述指针强制转换在单个赋值表达式的上下文中都是可接受的,但它实际上是合法的行为来保留实际上类型struct One*的转换指针指向struct Two类型的对象,并期望它可用于改变其目标成员或访问其目标已被突变的成员。使用指向struct One的指针的唯一上下文就好像它是指向struct Two前缀的指针一样,这两个对象重叠在union中。 / p>

---标准参考:

[1]“如果一个union包含几个共享一个公共初始序列的结构(见下文),并且如果union对象当前包含这些结构中的一个,则允许在任何地方检查它们中的任何一个的公共初始部分可以看到已完成的工会类型的声明。“

[2]“存储在任何其他对象类型的非位字段对象中的值由n×CHAR_BIT组成 bits,其中n是该类型对象的大小,以字节为单位。该值可以复制到 unsigned char [n]类型的对象(例如,通过memcpy)......“

[3]“指向对象类型的指针可以转换为指向不同对象类型的指针......当指向对象的指针转换为指向字符类型的指针时,结果指向最低的字节类型对象。结果的连续递增,直到对象的大小,产生指向对象剩余字节的指针。“

答案 1 :(得分:0)

C99 6.7.2.1说:

第5段

  

如6.2.5中所述,结构是由序列组成的类型   成员,其存储按有序顺序分配

第12段

  

结构或联合对象的每个非位字段成员都对齐   一种适合其类型的实现定义方式。

第13段

  

在结构对象中,非位域成员和单位   哪些位字段的地址会按顺序增加   他们被宣布。适当地指向结构对象的指针   转换后,指向其初始成员(或者如果该成员是   bit-fi eld,然后到它所在的单位),反之亦然。那里   可能是结构对象中的未命名填充,但不是在其中   开始

最后一段介绍了你的第二个问题(将指针指向One转换为int *,并取消引用它)。

第一点 - 是否有效“向下”Two*One* - 我找不到具体解决的问题。归结为其他规则是否确保One的字段和Two的初始字段的内存布局在所有情况下都是相同的。

成员必须按有序顺序打包,开头不允许填充,并且必须根据类型对齐,但​​标准实际上并没有说布局需要相同(即使在大多数编译器我相信它是。)

然而,有一种更好的方法来定义这些结构,以便您可以保证:

struct One
{
        int Hurr;
        char Durr[2];
        float Nrrr;
} One;

struct Two
{
        struct One one;
        double Wibble;
} Two;

您可能认为现在可以安全地将Two*投射到One* - 第13段说明了这一点。但是strict aliasing可能会让你感到不愉快。但是通过上面的例子你无论如何都不需要:

One = Two.one;

答案 2 :(得分:-1)

A1。未定义的行为,因为Wibble。 A2。定义

N3337中的S9.2。

  

两种标准布局结构(第9节)类型是布局兼容的   它们具有相同数量的非静态数据成员并且相应   非静态数据成员(按声明顺序)具有布局兼容性   类型

你的结构将是布局兼容的,因此可以互换但是对于Wibble。也有一个很好的理由:Wibble可能会导致struct 2中的填充不同。

  

指向标准布局结构对象的指针,适当地使用转换   reinterpret_cast指向其初始成员(或者如果该成员是   比特字段,然后到它所在的单位,反之亦然。

我认为可以保证你可以取消引用初始的int。