有很多像这样的代码:
#include <stdio.h>
int main(void)
{
int a[2][2] = {{0, 1}, {2, -1}};
int *p = &a[0][0];
while (*p != -1) {
printf("%d\n", *p);
p++;
}
return 0;
}
但基于此answer,行为未定义。
N1570。 6.5.6 p8:
添加或减去具有整数类型的表达式时 从指针开始,结果具有指针操作数的类型。如果 指针操作数指向数组对象的元素和数组 足够大,结果指向一个偏离的元素 原始元素使得下标的差异 结果和原始数组元素等于整数表达式。 换句话说,如果表达式P指向一个的第i个元素 数组对象,表达式(P)+ N(等效地,N +(P))和(P)-N (其中N具有值n)分别指向第i + n和第i 数组对象的第i个元素,只要它们存在即可。此外, 如果表达式P指向数组对象的最后一个元素,则 表达式(P)+1指向数组对象的最后一个元素, 如果表达式Q指向一个数组的最后一个元素 对象,表达式(Q)-1指向数组的最后一个元素 宾语。如果指针操作数和结果都指向元素 相同的数组对象,或一个超过数组的最后一个元素 对象,评估不得产生溢出;否则, 行为未定义。如果结果指向最后一个元素 对于数组对象,它不应该用作一元的操作数 *被评估的运算符。
有人可以详细解释一下吗?
答案 0 :(得分:9)
指定了基址(指向第一个元素的指针)p
的数组的类型为int[2]
。这意味着p
中的地址只能在*p
和*(p+1)
位置合法地取消引用,或者如果您更喜欢下标符号,p[0]
和{ {1}}。此外,p[1]
保证合法地评估作为地址,并且可比较到该序列中的其他地址,但不被解除引用。这是一个过去的地址。
您发布的代码违反了过去的规则,一旦它传递了归属的阵列中的最后一个元素,就会解除引用p+2
。它所归属的数组与另一个相似维度的数组相对应,与所引用的正式定义无关。
那就是说,在练习中它起作用,但正如常说的那样。 观察到的行为不是,也不应该被认为是定义的行为。仅仅因为它的工作原理并不合适。
答案 1 :(得分:4)
指针的对象表示是不透明的,在C中。没有禁止对具有编码的边界信息的指针。这是记住的一种可能性。
更实际的是,实现还能够基于由以下规则声明的假设来实现某些优化:别名。
然后保护程序员免受意外伤害。
在函数体内考虑以下代码:
struct {
char c;
int i;
} foo;
char * cp1 = (char *) &foo;
char * cp2 = &foo.c;
鉴于此, cp1 和 cp2 将相等,但它们的界限仍然不同。 cp1 可以指向 foo 的任何字节,甚至可以指向&#34;一个过去&#34; foo ,但 cp2 只能指向&#34;一个过去&#34; foo.c ,最多,如果我们希望保持定义的行为。
在此示例中, foo.c 和 foo.i 成员之间可能存在填充。虽然填充的第一个字节与&#34;一个过去&#34; foo.c 成员, cp2 + 2 可能会指向其他填充。实现可以在翻译期间注意到这一点,而不是制作程序,它可以告诉您,您可能正在做一些您认为自己没做过的事情。
相比之下,如果您阅读 cp1 指针的初始值设定项,它会直观地表明它可以访问 foo 结构的任何字节,包括填充。
总之,这可能会在转换(警告或错误)或程序执行期间(通过编码边界信息)产生未定义的行为;标准方面没有区别:行为未定义。
答案 2 :(得分:1)
您可以将指针转换为指向数组指针的指针,以确保正确的数组语义。
此代码确实没有定义,但在今天常用的每个编译器中作为C扩展提供。
然而,正确的方法是将指针转换为指向数组的指针,如下所示:
((int(*)[2])p)[0] [0]
获取第0个元素或说:
((int(*)[2])p)[1] [1]
得到最后一个。
要严格,他认为这是非法的,因为你正在打破严格的别名,指向不同类型的指针可能不会指向同一个地址(变量)。
在这种情况下,您正在创建一个指向int数组的指针和一个指向int的指针并将它们指向相同的值,标准不允许这样做,因为唯一可以别名另一个指针的类型是char *甚至这很少用得恰到好处。