使用单个指针访问2D数组

有很多像这样的代码:

#include  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; } 

但基于这个答案 ,行为是不确定的。

N1570。 6.5.6 p8:

当一个具有整数类型的表达式被添加到指针或从指针中减去时,结果具有指针操作数的类型。 如果指针操作数指向数组对象的元素,并且数组足够大,则结果指向与原始元素偏移的元素,使得结果元素和原始数组元素的下标的差异等于整数表达式。 换句话说,如果表达式P指向数组对象的第i个元素,则表达式(P)+ N(等效地,N +(P))和(P)-N(其中N具有值n)指向分别为数组对象的第i + n和第i-n个元素,只要它们存在。 此外,如果表达式P指向数组对象的最后一个元素,则表达式(P)+1指向一个超过数组对象的最后一个元素,如果表达式Q指向一个超过数组对象的最后一个元素,表达式(Q)-1指向数组对象的最后一个元素。 如果指针操作数和结果都指向同一个数组对象的元素,或者指向数组对象的最后一个元素,则评估不应产生溢出; 否则,行为未定义。 如果结果指向数组对象的最后一个元素之后,则不应将其用作已计算的一元*运算符的操作数。

有人可以详细解释一下吗?

指定了基址(指向第一个元素的指针) p的数组的类型为int[2] 。 这意味着p的地址只能在*p*(p+1)位置合法地解除引用 ,或者如果你更喜欢下标符号, p[0]p[1] 。 此外,保证p+2在法律上被评估为地址,并且该序列中的其他地址相当 ,但不能被解除引用。 这是一个过去的地址。

您发布的代码违反了过去的规则,一旦它传递了归属的数组中的最后一个元素,就会取消引用它。 它所归属的数组与另一个相似维度的数组相对应,与所引用的forms定义无关。

也就是说,在实践中它起作用,但正如人们常说的那样。 观察到的行为不是,也不应该被认为是定义的行为 。 仅仅因为它的工作原理并不合适。

指针的对象表示是不透明的,在C中。没有禁止对具有编码的边界信息的指针。 这是一种记住的可能性。

更实际地,实现还能够基于由以下规则声明的假设来实现某些优化:别名。

然后是程序员免受事故的保护。

在函数体内考虑以下代码:

 struct { char c; int i; } foo; char * cp1 = (char *) &foo; char * cp2 = &foo.c; 

鉴于此, cp1cp2将相等,但它们的界限仍然不同。 cp1可以指向foo的任何字节,甚至指向“一个过去”的foo ,但是如果我们希望保持定义的行为,则cp2最多只能指向“一个过去” foo.c。

在此示例中, foo.cfoo.i成员之间可能存在填充。 虽然该填充的第一个字节与foo.c成员的“一个过去”共同包含,但cp2 + 2可能指向另一个填充。 实现可以在翻译期间注意到这一点,而不是生成程序,它可以告诉您,您可能正在做一些您认为自己没做过的事情。

相比之下,如果您读取cp1指针的初始化程序,它直观地表明它可以访问foo结构的任何字节,包括填充。

总之,这可能会在转换(警告或错误)或程序执行期间(通过编码边界信息)产生未定义的行为; 标准方面没有区别:行为未定义。

您可以将指针转换为指向数组指针的指针,以确保正确的数组语义。

这个代码确实没有定义,但在今天常用的每个编译器中作为C扩展提供。

但是,正确的方法是将指针转换为指向数组的指针,如下所示:

((int(*)[2])p)[0] [0]

获得第0个元素或说:

((int(*)[2])p)[1] [1]

得到最后一个。

要严格,他认为这是非法的,因为你正在打破严格的别名,指向不同类型的指针可能不会指向同一个地址(变量)。

在这种情况下,您正在创建一个指向int数组的指针和一个指向int的指针并将它们指向相同的值,标准不允许这样做,因为可能别名另一个指针的唯一类型是char *甚至这个很少使用得当。