为什么指针artihmetic与非连续的2d数组一起工作?

时间:2018-01-20 19:49:26

标签: c arrays pointers memory pointer-arithmetic

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

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

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

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

2 个答案:

答案 0 :(得分: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]等于{{1 }}

使用上面没有数组数组的“公式”,*(a + i)得到的是另一个数组。即aa[i]是另一个数组,您可以使用索引,例如*(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]值。

使用指针指针实现的“二维”数组(实际上并非如此)也称为jagged array,并且它不必是连续的。这意味着int表达式确实无效。

再举一个简单的例子:

*(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 } 类似的“二维”数组。最大的区别是它的内存布局,类似于

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

对于外部数组,aa仍然等于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]也是如此。

答案 1 :(得分:2)

使用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 ,数组会自动转换为指向其第一个元素的指针。我们称之为pA是一个M元素数组,每个元素都是N SomeType元素的数组。因此p是指向N的第一个SomeType元素数组的指针。
  • 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指向的是N SomeType元素的数组。
  • 由于q指向数组,*q 数组。
  • 此数组自动转换为指向其第一个元素的指针。我们称之为rr指向数组q的第一个元素指向。
  • 现在我们有(r)[j],或者删除括号r[j],其中r指向i元素A的数组的元素0 }。
  • 同样,下标的定义表明这与(*(r+j))相同。
  • 通过指针算术r+j指向数组的元素j
  • 由于r+j指向元素j*(r+j) 数组的j元素。
  • 因此,A[i][j]j中由i编制索引的数组的元素A

现在考虑使用指针指针实现的二维数组,如下代码所示:

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元素移动。在这种情况下,A+i指向指针(特别是指向SomeType的指针),因此指针算术的元素就是那些指针。所以i指向超出第一个指针的q个指针。我们称之为(*q)[j]
  • 现在我们有q,其中i指向我们创建的指针数组中的元素q
  • 由于*q指向指针,r是指针。我们称之为rSomeType指向分配了malloc次调用之一的第一个元素((r)[j])。
  • 现在我们有r[j],或者删除括号r,其中i指向指针数组中的元素(*(r+j))
  • 同样,下标的定义表明这与r+j相同。
  • 通过指针算术j指向第一个元素r所指向的数组的元素r+j
  • 由于j指向元素*(r+j)j 数组的A[i][j]元素。
  • 因此,ji中由A编制索引的数组的元素sizeof

脚注

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