指针到T,T数组和指向数组的T之间是否有未定义的行为?

时间:2014-08-28 01:13:16

标签: c++ c language-lawyer

请考虑以下代码。

#include <stdio.h>
int main() {
 typedef int T;
 T a[] = { 1, 2, 3, 4, 5, 6 };
 T(*pa1)[6] = (T(*)[6])a;
 T(*pa2)[3][2] = (T(*)[3][2])a;
 T(*pa3)[1][2][3] = (T(*)[1][2][3])a;
 T *p = a;
 T *p1 = *pa1;
 //T *p2 = *pa2; //error in c++
 //T *p3 = *pa3; //error in c++
 T *p2 = **pa2;
 T *p3 = ***pa3;
 printf("%p %p %p %p %p %p %p\n", a, pa1, pa2, pa3, p, p1, p2, p3);
 printf("%d %d %d %d %d %d %d\n", a[5], (*pa1)[5], 
   (*pa2)[2][1], (*pa3)[0][1][2], p[5], p1[5], p2[5], p3[5]);
 return 0;
}

上面的代码在C中编译并运行,产生预期的结果。所有指针值都是相同的,所有int值都是相同的。我认为对于任何类型T,结果都是相同的,但int是最容易使用的。

我承认最初感到惊讶的是,取消引用指向数组的指针会产生相同的指针值,但在反射时我认为这仅仅是我们所知道和喜爱的数组到指针衰变的反转。

[编辑:注释掉的行会触发C ++中的错误和C中的警告。我发现C标准在这一点上含糊不清,但这不是真正的问题。]

this问题中,它声称是未定义的行为,但我看不到它。我是对的吗?

代码here,如果你想看到它。


在我写完上面之后,我突然意识到这些错误是因为C ++中只有一级指针衰减。需要更多解除引用!

 T *p2 = **pa2; //no error in c or c++
 T *p3 = ***pa3; //no error in c or c++

在我完成这个编辑之前,@ AntonSavin提供了相同的答案。我编辑了代码以反映这些变化。

3 个答案:

答案 0 :(得分:2)

更新: 以下内容仅适用于C ++ ,适用于C向下滚动。 简而言之,C ++中没有UB,C中有 UB

8.3.4/7说:

多维数组遵循一致的规则。 如果E是秩i x j x ... x k的n维数组, 然后出现在受阵列到指针转换(4.2)的表达式中的E被转换为a 指向具有秩j x ... x k的(n-1)维数组的指针。如果是 * 运算符,则显式或隐式 作为订阅的结果,应用于此指针,结果是指向(n - 1)维数组, 它本身会立即转换为指针。

所以这不会在C ++中产生错误(并且会按预期工作):

T *p2 = **pa2;
T *p3 = ***pa3;

关于这是否是UB。考虑第一次转换:

T(*pa1)[6] = (T(*)[6])a;

在C ++中它实际上是

T(*pa1)[6] = reinterpret_cast<T(*)[6]>(a);

这就是标准所说的reinterpret_cast

可以将对象指针显式转换为不同类型的对象指针。当一个prvalue “指向T1的指针”类型的v被转换为“指向cv T2的指针”类型,结果是static_cast&lt;简历 T2 * &gt;(static_cast&lt; cv void * &gt;(v))如果T1和T2都是标准布局类型(3.9)和对齐 T2的要求不比T1严格,或者任何一种类型无效。

因此a已转换为pa1static_castvoid*并返回。静态转换为void*保证会返回a中所述的4.10/2的实际地址:

类型为“指向cv T的指针”的prvalue,其中T是对象类型,可以转换为类型为“指针”的prvalue cv void“。将指针的非空指针值转换为对象类型的结果为“指针指向 cv void“表示内存中与原始指针值相同的字节的地址。

再次向T(*)[6]的静态广告投放保证会返回5.2.9/13中所述的相同地址:

类型为“指向cv1 void的指针”的prvalue可以转换为“指向cv2 T的指针”的prvalue,其中T是 对象类型和cv2是相同的cv-qualification,或者比cv1更高的cv资格。空指针 value被转换为目标类型的空指针值。如果原始指针值表示 存储器中A字节的地址A和A满足T的对齐要求,然后是结果指针 value表示与原始指针值相同的地址,即A

因此保证pa1指向内存中与a相同的字节,因此通过它访问数据是完全有效的,因为数组的对齐方式与底层类型的对齐方式相同

C怎么样?

再考虑一下:

T(*pa1)[6] = (T(*)[6])a;

在C11标准中,6.3.2.3/7声明如下:

指向对象类型的指针可以转换为指向不同对象类型的指针。如果 结果指针未正确对齐引用类型,行为是 未定义。否则,当再次转换回来时,结果应该等于 原始指针。当指向对象的指针转换为指向字符类型的指针时, 结果指向对象的最低寻址字节。连续增量 结果,直到对象的大小,产生指向对象剩余字节的指针。

这意味着除非转换为char*,否则转换指针的值不能保证等于原始指针的值,从而导致未定义的行为通过转换指针访问数据时。为了使其有效,转换必须通过void*

明确完成
T(*pa1)[6] = (T(*)[6])(void*)a;

转换回T *

T *p = a;
T *p1 = *pa1;
T *p2 = **pa2;
T *p3 = ***pa3;

所有这些都是从array of Tpointer to T的转换,它们在C ++和C中均有效,并且通过转换指针访问数据不会触发UB。

答案 1 :(得分:2)

您的代码在C中编译的唯一原因是您的默认编译器设置允许编译器隐式执行某些非法指针转换。形式上,C语言不允许这样做。这些行

T *p2 = *pa2;
T *p3 = *pa3;

在C ++中是格式错误的并在C中产生约束违规。用简单的说法,这些行在C和C ++中都是错误语言。

任何自尊的C编译器都会针对这些约束违规发出(实际上必需发布)诊断消息。例如,GCC编译器将发出&#34;警告&#34;告诉你上面的初始化中的指针类型是不兼容的。而#34;警告&#34;完全足以满足标准要求,如果你真的想使用GCC编译器识别违反C代码的约束的能力,你必须使用-pedantic-errors开关运行它,最好明确选择标准语言版本使用-std=开关。

在您的实验中,C编译器为您作为非标准编译器扩展执行了这些隐式转换。但是,在ideone前端运行的GCC编译器完全抑制了相应的警告消息(即使在其默认配置中由独立GCC编译器发出),这意味着ideone是一个损坏的C编译器。无法有效地依赖其诊断输出来告知无效的C代码。

至于转换本身......执行此转换并不是未定义的行为。但是通过转换的指针访问数组数据是未定义的行为。

答案 2 :(得分:2)

这是一个仅限C的答案。

C11(n1570)6.3.2.3 p7

  

指向对象类型的指针可以转换为指向不同对象类型的指针。如果结果指针未正确对齐引用类型 *),则行为未定义。否则,当再次转换回来时,结果将与原始指针进行比较。

     

*)一般来说,“正确对齐”这个概念是可传递的:如果指向类型A的指针正确对齐指针类型为B,那么对于指向类型C的指针,转向正确对齐,然后指向类型A的指针正确对齐,以指向指向C的指针。

如果我们使用这样的指针(严格别名除外)除了将其转换回来之外的其他任何事情,标准有点模糊,但意图和广泛的解释是这样的指针应该相等(并且具有相同的)数值,例如,在转换为uintptr_t时它们也应该相等),例如,考虑(void *)array == (void *)&array(转换为char *而不是void *明确保证有效)。

T(*pa1)[6] = (T(*)[6])a;

这很好,指针正确对齐(它与&a指针相同)。

T(*pa2)[3][2] = (T(*)[3][2])a; // (i)
T(*pa3)[1][2][3] = (T(*)[1][2][3])a; // (ii)

Iff T[6]T[3][2]具有相同的对齐要求,与T[1][2][3](i)(ii)的对齐要求分别是安全的。对我来说,听起来很奇怪,他们不能,但我无法在标准中找到他们应该具有相同对齐要求的保证。

T *p = a; // safe, of course
T *p1 = *pa1; // *pa1 has type T[6], after lvalue conversion it's T*, OK
T *p2 = **pa2; // **pa2 has type T[2], or T* after conversion, OK
T *p3 = ***pa3; // ***pa3, has type T[3], T* after conversion, OK

忽略因int * printf所期望的void *而导致的UB,让我们看看下一个printf的参数中的表达式,首先是已定义的:< / p>

a[5] // OK, of course
(*pa1)[5]
(*pa2)[2][1]
(*pa3)[0][1][2]
p[5] // same as a[5]
p1[5]

请注意,此处严格别名不是问题,不会涉及错误输入的左值,我们会将T作为T访问。

以下表达式取决于越界指针算法的解释,更宽松的解释(允许container_ofarray flattening,带有char[]的“struct hack”等。)也允许他们;更严格的解释(允许可靠的运行时边界检查实现指针算术和解除引用,但不允许container_of,数组展平(但不一定是数组“提升”,你做了什么),结构黑客等等。 )使它们未定义:

p2[5] // UB, p2 points to the first element of a T[2] array
p3[5] // UB, p3 points to the first element of a T[3] array