【问题标题】:Why does pointer artihmetic work with non-contiguous 2d arrays?为什么指针算术适用于非连续二维数组?
【发布时间】:2025-12-23 11:45:06
【问题描述】:

我的理解是,如果一个人在本地声明一个二维数组:int 2darr[x][y],它不是一个指针数组,其中每个指针都指向自己的一维数组,而是处理器在其上执行的一维数组*(2darr + (row x nCols) + col) 类型的指针算法。

在这种情况下,语法糖 2darr[row][col] 背后的指针算法是有意义的,因为我们的二维数组实际上只是一个大小为 nRows x nCols 的连续内存块。

然而,动态分配二维数组的一种方法是首先分配一个大小为nRows 的指针数组,然后为每个指针分配一个大小为nCols 的任意类型的数组。在这种情况下,我们的行不一定会连续存储在内存中。每一行都可以存储在内存中完全不同的位置,我们的指针数组中的一个指针指向它的第一个元素。

鉴于此,我不明白我们如何仍然可以通过 2darr[row][col] 访问二维数组中的数据。由于不能保证我们的行是连续存储的,所以 *(2darr + (row x nCols) + col) 类型的指针算法根本不应该保证工作。

【问题讨论】:

  • 编译器将根据声明生成正确的访问算法。 int 2darr[x][y]int (*2darr)[y] 不同。关于指针和数组等价的各种介绍让很多人感到困惑,所以你不是第一个对此感到困惑的人;-)。连续的内存块是表示数组的更传统和更有效的方式。数组数组更灵活(例如,它允许不规则数组,其中不同的行可以有不同的长度)。
  • 您遇到了 C 语言中一个奇怪的不一致之处。
  • 答案似乎相当复杂。我认为最好这样看待:int x[10][20] 是一回事,指针数组是int ** y。下标运算符在数组和指针上的工作方式不同,我认为这是一个足够好的解释......

标签: c arrays pointers memory pointer-arithmetic


【解决方案1】:

使用SomeType A[M][N] 定义的数组和使用指向指针数组的指针实现的数组都可以作为A[i][j] 访问的原因在于下标运算符的工作方式、指针运算的工作方式以及自动将数组转换为指针。

一个关键的区别在于,在带有指针的A[i][j] 中,A[i] 是一个指针,它的值是从内存中获取然后与[j] 一起使用的。相比之下,在A[i][j] 与数组中,A[i] 是一个数组,其作为指针的值是基于数组本身的;表达式中数组的使用被转换为指向其第一个元素的指针。用于指针的A[i] 和用于数组的A[i] 都需要使用指针进行下一步,但第一个是从内存中的指针加载的,第二个是根据数组在内存中的存储位置计算的。

首先,考虑一个用以下方式定义的数组:

SomeType A[M][N];

鉴于此,当计算表达式 A[i][j] 时,计算继续进行:

  • A 是一个数组。
  • 在这种情况下1,数组会自动转换为指向其第一个元素的指针。我们称之为pAM 元素的数组,每个元素都是SomeTypeN 元素的数组。所以p 是指向SomeTypeN 元素的第一个数组的指针。
  • p 替换了 A,所以表达式现在是 p[i][j]
  • 下标的定义表明E1[E2](*(E1+E2)) 相同。 (为简洁起见,我省略了正式定义的括号。)当我们将其应用于第一个下标时,p[i][j] 变为 (*(p+i)[j]
  • 接下来,评估p+i。指针算术以指向类型为单位工作。由于p 指向N 元素的数组,p+i 从第一个数组(索引为0)移动到索引为i 的数组。我们称之为q
  • 现在我们有了(*q)[j],其中q 指向i 的元素A。请注意,此元素 q 指向的是 SomeTypeN 元素的数组。
  • 由于q 指向一个数组,所以*q 数组。
  • 此数组会自动转换为指向其第一个元素的指针。我们称之为rr 指向数组的第一个元素 q 指向。
  • 现在我们有了(r)[j],或者,去掉括号,r[j],其中r 指向数组的元素0,即i 的元素A
  • 再一次,下标的定义表明这与(*(r+j)) 相同。
  • 通过指针运算r+j指向数组的元素j
  • 由于r+j指向元素j*(r+j)数组的元素j
  • 因此A[i][j]A 中的i 索引的数组的元素j

现在考虑一个用指向指针的指针实现的二维数组,如下代码:

SomeType **A = malloc(M * sizeof *A);
for (size_t i = 0; i < M; ++j)
    A[i] = malloc(N * sizeof *A[i]);

(我们假设所有malloc 调用都成功。在生产代码中,应该对其进行测试。)

鉴于此,当计算表达式 A[i][j] 时,计算继续进行:

  • A 是指向SomeType 的指针。
  • 根据下标的定义,A[i][j] 等同于(*(A+i))[j]
  • 通过指针算术,A+iA 指向的位置移动到超出它的i 元素。在这种情况下,A 指向指针(特别是指向 SomeType 的指针),因此指针算术的元素就是那些指针。所以A+i 指向第一个指针之外的i 指针。我们称之为q
  • 现在我们有了(*q)[j],其中q 指向我们创建的指针数组中的元素i
  • 因为q 指向一个指针,所以*q 就是那个指针。我们称之为rr 指向分配给malloc 调用之一的第一个元素(SomeType)。
  • 现在我们有了(r)[j],或者,去掉括号,r[j],其​​中r 指向指针数组中的元素i
  • 再一次,下标的定义表明这与(*(r+j)) 相同。
  • 通过指针运算r+j 指向第一个元素r 指向的数组的元素j
  • 由于r+j指向元素j*(r+j)数组的元素j
  • 因此A[i][j]A 中的i 索引的数组的元素j

脚注

1 类型为“type 的数组”的表达式将转换为指向数组第一个元素的指针,除非它是sizeof 的操作数, _Alignof 或一元 &amp; 或者是用于初始化数组的字符串文字。

【讨论】:

【解决方案2】:

您的数组2darr 是一个数组的数组。

例如,像这样的定义

int aa[2][3];

是一个包含两个元素的数组,每个元素又是一个包含三个int 值的数组。

在内存中看起来像这样

+----------+----------+----------+---------+----- -----+----------+ | aa[0][0] | aa[0][1] | AA[0][2] | aa[1][0] | aa[1][1] | aa[1][2] | +----------+----------+----------+---------+----- -----+----------+

关于指针运算的部分可能会让您感到困惑,即对于任何数组(或指针!)a 和索引i,表达式a[i] 等于*(a + i)

使用上面没有数组数组的“公式”,aa[i] 得到的是另一个数组。 IE。 *(aa + i) 是另一个数组,您可以反过来使用索引,例如 (*(aa + i))[j]。第二级索引当然也可以使用指针算法编写,如*(*(aa + i) + j)

如果没有数组aa 将是*(aa + i * 3 + j),则显示的表达式对于数组数组是不正确的。我的意思是它在语义上是不正确的。这是因为*(aa + i * 3 + j)aa[i * 3 + j] 完全相同,在aa 的情况下是一个数组。表达式aa[i * 3 + j](因此*(aa + i * 3 + j))的类型为int[3]。它不是单个 int 元素。

你的表达式,*(a + row * ncol + col) 的形式只有在你有一个数组时才是正确的。喜欢

int bb[6];  // 6 = 2 * 3

现在这个数组可以使用*(bb + i * 3 + j)(或bb[i * 3 + j])进行索引,结果将是一个int值。


使用指向指针的指针实现的“二维”数组(实际上不是)也称为jagged array,它不必是连续的。这意味着*(2darr + (row x nCols) + col) 表达式确实无效。

再举一个简单的例子:

int **pp;

pp = malloc(sizeof *pp * 2);  // Two elements in the "outer" array
for (size_t i = 0; i < 2; ++i)
{
    pp[i] = malloc(sizeof **pp * 3);  // Three elements in the "inner" array
}

上面的代码创建了一个与上面的aa 类似的“二维”数组。最大的不同是它的内存布局,类似于

+-------+-------+ | pp[0] | pp[1] | +-------+-------+ | | | v | +----------+----------+----------+ | | pp[1][0] | pp[1][1] | pp[1][2] | | +----------+----------+----------+ v +----------+----------+----------+ | pp[0][0] | pp[0][1] | pp[0][2] | +----------+----------+----------+

对于外部数组,pp[i] 仍然等于 *(pp + i),但是虽然 aa[i] 生成一个包含三个 int 元素的数组,pp[i] 是指向 int 的指针(即 int * )。

由于您可以对指针使用数组索引语法,因此可以对来自pp[i] 的指针进行索引,然后您就可以使用“二维”语法pp[i][j]

虽然*(pp + i * 3 + j) 表达式无效,因为内存不是连续的,所以上面显示的所有其他指针算法都是有效的。例如(如图所示)pp[i] 等于 *(pp + i)。但由于这是一个可以索引的指针,(*(pp + i))[j] 也是有效的,*(*(pp + i) + j) 也是有效的。

【讨论】:

  • 谢谢,这很有趣,我认为声明为 aa[2][3] 的二维数组被编译器视为一维数组,因此应用了与一维数组相同的指针算法
  • 这个答案讨论了数组的数组。尽管 OP 对它们的工作方式有一些误解,但问题是问构造为指针指针的二维数组是如何工作的。
  • 这是很好的解释。 @jdigital 对您来说究竟有什么不合适的?我认为这很清楚。
  • @EricPostpischil 他们不会!数组是数组,而不是指针(尽管数组可以衰减指向它的第一个指针元素)。并且数组数组与指针指针相同。
  • @jdigital 如果*(aa + i) 是一个数组,它当然可以像一个一样被索引。而aa[i][j]等于*(aa[i] + j)。由于aa[i] 等于*(aa + i),那么aa[i][j] 等于*(*(aa + i) + j)。这只是简单的替换。