5

在尝试调试我使用 Speex 时遇到的问题时,我注意到它(不仅是 Speex,还有一些示例代码)执行以下操作:

  • 从初始化函数返回指向 EncState 的指针
  • 将该指针转换为 void 指针
  • 存储空指针
  • (别处)
  • 将 void 指针转换为指向 SpeexMode 的指针
  • 取消引用指针

碰巧 的定义以EncStatetype 的字段开头SpeexMode *,因此指向第一个字段的指针和指向 struct 的指针的整数值恰好相同。取消引用恰好在运行时起作用。

但是......语言真的允许这样做吗?如果编译它,编译器是否可以自由地做它想做的任何事情?如果 C`,将结构T*转换为结构C*未定义的行为T''s first field is a

4

2 回答 2

8

来自 C11 标准:

(C11 §6.7.2.1.15:“指向结构对象的指针,经过适当转换,指向其初始成员……反之亦然。结构对象内可能有未命名的填充,但不是在其开头。”)

这意味着您看到的行为是允许和保证的。

于 2013-01-24T21:20:06.023 回答
-1

每个版本的标准都将对许多别名结构的支持视为实施质量问题,因为基本上不可能编写支持所有有用结构、不阻止任何有用优化并且可以被所有编译器支持的规则无需大量返工。考虑以下函数:

struct foo {int length; int *dat; };

int test1(struct foo *p)
{
  int *ip = &p->length;
  *ip = 2;
  return p->length;      
}

我认为很明显,任何高质量的编译器都应该能够处理类型对象struct foo可能受到对*ip. 另一方面,考虑函数:

void test2(struct foo *p)
{
    int i;
    for (i=0; i < p->length; i++)
        p->dat[i] = 0;
}

是否应该要求编译器考虑到写入p->dat[i]可能影响 的值的可能性p->length,例如通过p->length至少在循环的第一次迭代之后重新加载 的值?

我认为委员会的一些成员可能打算要求编译器做出这样的允许,但我不认为他们都这样做了,而且所写的规则也不需要它,因为它们列出了可能用于的左值类型访问类型的对象struct foo,并且int不在其中。有些人可能认为遗漏是偶然的,但我认为这是基于编译器将规则解释为要求在某些上下文中作为某种特定类型访问的对象由具有可见关联的左值访问的期望在该上下文中具有所列类型之一的对象。什么构成“可见关联”的问题作为 QoI 问题留在了标准的管辖范围之外,但编译器编写者应该在可行时做出合理的努力来识别关联。

在类似的函数test1中,类型的左值p用于派生ip,并且p不以任何其他方式用于在p->length形成ip和最后一次使用之间进行访问。因此,编译器应该可以毫不费力地认识到,即使没有一般规则允许使用类型指针访问不相关结构的成员,*ip也无法在以后的 read to 中重新排序存储。但是,在 内部,没有可见的方法可以将 的地址用于计算指针,因此优化旨在用于最常见目的的编译器来提升对 的读取是合理的。p->lengthint*inttest2p->lengthp->datp->length在循环之前期望它的值不会改变。

clang 和 gcc 并没有做出任何努力来识别派生指针的对象的类型,而是选择表现得好像标准授予了使用其类型指针访问 struct(但不是 union!)成员的一般权限。这是允许的,但不是标准所要求的(符合标准但垃圾质量的实现可以test1以任意无意义的方式处理),但是对指针派生的盲目性不必要地限制了程序员可用的构造范围,并且有必要放弃应该有用的东西优化,例如test2().

总的来说,几乎所有与 C 中的别名相关的问题的正确答案都是“这是一个实现质量问题”。观察 clang 和 gcc 的作用可能对需要安抚-fstrict-aliasing这些编译器模式的人有用,但与标准的实际内容无关。

于 2021-02-20T19:30:58.620 回答