【问题标题】:Accesing a 2D array using a single pointer使用单个指针访问二维数组
【发布时间】:2014-10-07 20:16:48
【问题描述】:

有很多这样的代码:

#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 指向一个 数组对象,表达式 (P)+N(等价于 N+(P))和 (P)-N (其中 N 的值为 n)分别指向第 i+n 个和 数组对象的第 i-n 个元素,前提是它们存在。而且, 如果表达式 P 指向数组对象的最后一个元素,则 表达式 (P)+1 指向数组对象的最后一个元素, 如果表达式 Q 指向数组的最后一个元素 对象,表达式 (Q)-1 指向数组的最后一个元素 目的。如果指针操作数和结果都指向元素 相同的数组对象,或数组的最后一个元素 对象,评估不应产生溢出;否则, 行为未定义。如果结果指向最后一个元素 数组对象,不得用作一元的操作数 * 被评估的运算符。

谁能详细解释一下?

【问题讨论】:

  • 您在链接的问题中阅读了 christoph 的回答吗? stackoverflow.com/a/7787436/3684343我觉得他解释的很好。
  • 是的,有大量带有 UB 的代码。
  • 与指针 向后 遍历数组并排排列,直到其值“小于”第一个元素地址。它的惊人它的频率有多高,几乎没有人知道他们这样做是在调用 UB。
  • 我在 N1570 中找不到任何东西来证明这条规则的合理性,除了“因为标准这么说”。似乎数组订阅和sizeof 规则阻止了不同数组维度之间的任何类型的填充。我想知道是否有任何标准编译系统会破坏上述代码。
  • 现在我考虑到这一点,也许这个限制的目的是允许将子阵列放置在不同的内存库上,就像 PIC 一样。所以 a[0] 和 a[1] 可能被放在不同的 bank 上,示例代码会失败,因为编译器假定循环中不需要 bank 切换指令。

标签: c arrays pointers


【解决方案1】:

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

您发布的代码通过取消引用 p 一旦传递了它所在的数组中的最后一个元素,就违反了过去的规则。它所在的数组与另一个类似维度的数组相互支撑,这与引用的正式定义无关。

也就是说,在实践中它是有效的,但正如人们常说的那样。 观察到的行为不是,也不应该被认为是定义的行为。仅仅因为它有效并不能使它正确。

【讨论】:

  • 谢谢你,我知道我可以检查if (p + 3) {,但我不能取消引用int x = *(p + 3);,对吧?
  • 不,即使地址 value p+3 用于评估、比较或取消引用也是不合法的。它超出了 a[0] 的地址范围。 .. a[0]+2(后者是int[2]数组a[0]的过去一次地址)。
  • @WhozCraig 我来自回答stackoverflow.com/questions/29666141/… 并想知道谁是对的(互联网上有人错了!xkcd.com/386)。 a 不保证占用连续内存吗?我可以肯定地通过 char* 合法地遍历 a。考虑到 n1570 在 6.5,7 中关于聚合的递归豁免,int* 也没有别名问题。那么你在哪里找到访问p+3 是UB 的措辞? iyo 会将a 的地址直接转换为 int* 会产生差异吗?
【解决方案2】:

在 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 结构的任何字节,包括填充。

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

【讨论】:

  • 我认为您的意思是“填充字节”。是的,但没有要求在实现上区分这两种情况。因此,如果它可以拒绝翻译并在一个示例中向您发出警告,它可以在另一个示例中执行相同的操作。这个想法是:编程你的意思,而不是可能发生或应该起作用的东西。 (int *) &a 就可以了。
【解决方案3】:

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

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

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

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

获取第零个元素或说:

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

获得最后一个。

严格来说,我认为这是非法的原因是你打破了严格的别名,指向不同类型的指针可能不会指向同一个地址(变量)。

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

【讨论】:

  • 严格别名表示一种类型的值表示可能不会被读取为另一种类型的内存(除了一些允许的别名)。将int 读作int 总是可以的,即使一个是聚合成员而另一个不是。 restrict 涵盖了指向不同类型的指针指向重叠存储的想法。
猜你喜欢
  • 2013-06-29
  • 1970-01-01
  • 1970-01-01
  • 2016-01-01
  • 1970-01-01
  • 1970-01-01
  • 2021-10-30
  • 1970-01-01
  • 1970-01-01
相关资源
最近更新 更多