使用字符类型上次写入时,使用非字符类型读取对象时的未定义行为

假设unsigned int没有陷阱表示,请执行下面标记为(A)和(B)的语句中的任何一个或两个引发未定义的行为,为什么或为什么不行为,以及(特别是如果您认为其中一个定义明确但另一个不定义)是的,您认为标准中存在缺陷吗? 我主要对当前版本的C标准(即C2011)感兴趣,但如果在标准的旧版本或C ++中有所不同,我也想知道这一点。

_Alignas在这个程序中被用来消除因对齐不充分而导致的任何UB问题。我在解释中讨论的规则虽然没有说明对齐。)

 #include  #include  int main(void) { unsigned int v1, v2; unsigned char _Alignas(unsigned int) b1[sizeof(unsigned int)]; unsigned char *b2 = malloc(sizeof(unsigned int)); if (!b2) return 1; memset(b1, 0x55, sizeof(unsigned int)); memset(b2, 0x55, sizeof(unsigned int)); v1 = *(unsigned int *)b1; /* (A) */ v2 = *(unsigned int *)b2; /* (B) */ return !(v1 == v2); } 

我对C2011的解释是(A)引发未定义的行为,但(B)是明确定义的(将未指定的值存储到v2 ),因为:

  • memset被定义为(第7.24.6.1节),以便通过具有字符类型的左值来写入其第一个参数,根据§6.5p7底部的特殊情况, b1b2都允许这样做。

  • 对象b1具有声明的类型unsigned char[n] 。 因此,它的有效访问类型也是每6.5p6的unsigned char[n] 。 语句(A)通过左值表达式读取b1 ,该表达式的类型为unsigned int ,这不是b1的有效类型,也不是6.5p7中的任何其他exception,因此行为未定义。

  • b2指向的对象没有声明的类型。 存储在其中的值(通过memset )是(as-if)通过具有字符类型的左值,因此第二种情况6.5p6不适用。 该值未从任何地方复制 ,因此第三种情况6.5p6也不适用。 因此,对象的有效类型是用于访问的左值的类型,它是unsigned int ,并且满足6.5p7的规则。

  • 最后,根据6.2.6.1,假设unsigned int没有陷阱表示, memset操作在b1b2每一个中创建了一些未指定的unsigned int值的表示。 因此,如果(A)和(B)都没有引起未定义的行为,那么v1v2中的实际值是未指定的,但它们是相等的。

评论:

“基于类型的别名”规则(即6.5p7)的不对称性允许具有任何有效类型的对象由具有字符类型的左值访问,但反之亦然,这是混乱的持续来源。 6.5p6的第二种情况似乎是专门添加的,以防止它是未定义的行为来读取由memset初始化的值(或者,就此而言, calloc ),但是,因为它只适用于没有声明类型的对象,它本身就是另一个混乱的来源。

标准的作者在理由中承认,实施可能符合但无用。 因为他们期望实现者努力使他们的实现有用,他们认为没有必要强制要求实现适合任何特定目的的实现所需的每个行为。

标准对访问字符数组类型的对齐对象的代码行为没有任何要求,如同其他类型一样。 这并不意味着他们希望实现除了将代码作为数据的地址一次但从不直接访问它的情况下将数组视为无类型存储之外应该做的事情。 别名的基本特征是它要求以两种不同的方式访问项目; 如果只以一种方式访问​​对象,则根据定义没有别名。 任何适用于低级编程的高质量实现都应该以有用的方式运行,如果char[]仅用作无类型存储, 无论标准是否需要它 ,并且很难想象任何有用的目的会受到这种待遇的阻碍。 使标准授权这种行为所能达到的唯一目的是防止编译器编写者将缺乏授权视为本身 – 而不是以明显有用的方式处理此类代码的理由。

在表面检查中,我同意你的评估(A是UB,B很好),并且可以提供一个具体的理由来解释为什么应该这样(在编辑之前包括_Alignas() ): 对齐

堆栈上的char[]可以从任何地址开始,无论这是否是unsigned int的有效对齐。 相反, malloc()需要返回满足所讨论平台上任何本机类型的最严格对齐要求的内存。

标准显然不希望在char[]之外强加对齐要求,因此它必须保留对它的类型惩罚访问权限,因为它可能是未定义的。