(*prt)[N][N] 在处理堆分配内存时如何工作?

How does (*prt)[N][N] work when dealing with heap allocation memory?

今天,一位同事向我展示了一种声明二维数组的方法,我可以线性分配它,但仍然使用二维方括号 ([][]) 表示法来访问元素。

例如:

#include <stdio.h>
#include <stdlib.h>

#define SIZE 2

int main () {
  int (*a)[SIZE][SIZE] = malloc (sizeof (int) * SIZE * SIZE);

  for (int i = 0; i < SIZE; i++) {
    for (int j = 0; j < SIZE; j++) {
      (*a)[i][j] = 0;
    }
  }

  (*a)[0][1] = 100;

  /* should yield:
   *   0
   *   100
   *   0
   *   0
   */
  for (int i = 0; i < SIZE; i++) {
    for (int j = 0; j < SIZE; j++) {
      printf ("%d\n", (*a)[i][j]);
    }
  }

  free (a);

  return EXIT_SUCCESS;
}

这与计算索引然后执行指针运算(例如 *(a + (x * SIZE + y)) 或更严格的 a[x * SIZE + y])以访问元素形成对比。

关键部分是指针 x 的形状声明(例如 (*x)[][]),它似乎将此信息编码为 x 指向的值的类型。

除此之外,我不明白这是如何工作的。这个符号到底在做什么?是语法糖吗?它看起来类似于数组的动态堆栈分配(参见 Array size at run time without dynamic allocation is allowed? 作为一个例子),但显然这种分配是在堆上发生的。

我已经查找了有关此指针 notation/declaration 的更多信息,但除了即将出现的术语 元素类型 之外找不到更多信息 - 但我'我不确定这是否相关。

编辑#1:

我应该提到这个问题是在使用堆而不是堆栈的上下文中。我知道基于堆栈的数组动态分配,但我正在做的工作专门研究动态内存分配。

int (*a)[SIZE][SIZE] = malloc (sizeof (int) * SIZE * SIZE); 所做的是声明一个指向二维整数数组的指针。这仅在您有意要在堆而不是堆栈中分配 space 时才有用(例如,如果数组的维度在编译时未知)然后您将取消引用指针并访问它就像处理普通的二维数组一样。

您可以通过将变量声明为指针数组来跳过取消引用步骤,每个指针都指向一个标准的整数数组 int *a[SIZE],甚至 int **a。在这两种情况下,您都可以使用括号表示法 a[x][y] 访问任何值,而无需在之前取消引用 a

如果你在编译时就知道数组的维度,并且不需要在堆中分配它,你可以像这样声明数组:

int a[SIZE][SIZE];

因为它在堆栈中分配了 space,所以它既短又高效。

您始终可以使用 [][] 访问数组。 你必须记住,C 中的所有内容都与内存地址偏移有关。当你有一个整数数组声明为 int a[4] 并且你用方括号访问它时 a[3] 你告诉处理器获取 a 的内存地址并应用偏移量 3 * sizeof(int)。您可以使用 *(&a + 3) 甚至 3[a] 访问相同的元素,因为获取地址并添加偏移量与获取偏移量并添加地址相同。

因此,当您使用 a[2][3] 时,编译器与上面的操作完全相同,只是维度更多。所以你不需要做 a[x * SIZE + y] 因为这正是编译器在你做 a[x][y].

时为你做的

编辑:正如一些人在评论中指出的那样,实际上指针不一定存储内存引用,尽管这绝对是最常见的实现。

我希望我的解释很清楚。

int (*a)[SIZE][SIZE] 是指向类型 int[SIZE][SIZE] 数组的 数组指针 。这是一种特殊的指针,用于指向整个数组,但在其他方面与任何其他指针一样工作。所以当你写 (*a)[i][j] 你说 "give me the contents of the pointer (the 2D array), then in those contents give me item number [i][j]".

但是由于数组指针的行为与其他指针一样,您可以使用它指向第一个元素而不是整个二维数组。 (就像您可以使用 int* 指向 int[n] 数组的第一项一样。)这是使用省略最左边维度的技巧完成的:int (*a)[SIZE] = ...。这现在指向数组数组中的第一个一维数组。现在您可以将其用作 a[i][j],这样可读性和方便性都更高。

我对 .

的回答中提到了数组指针、上述技巧以及如何使用它们将二维数组动态分配为一个单独的内存块

这并没有错,但不是更常见的(和惯用的方式)。要声明一个大小为 N 的动态数组,您可以使用:int *arr = malloc(N * sizeof(int));。事实上,这将 arr 声明为 指向 N int 数组的第一个元素的指针。二维数组是数组的数组,所以要声明一个二维数组 N*N,更常见的方式是:

int (*arr)[N] = malloc(N * N * sizeof(int));

这实际上将arr声明为指向N个int数组的第一个元素的指针。然后您可以正常使用 arr[i][j].

那么神奇的是什么int (*a)[SIZE][SIZE] = malloc (sizeof (int) * SIZE * SIZE);

您将 arr 声明为指向 NxN 整数二维数组的第一个(也是单个)元素的指针。好消息是声明对于所有维度的大小都是明确的,但缺点是您必须始终取消引用它:(*arr)[i][j] 这与 每个 [] 的定义没有区别来自 arr[0][i][j].

的 C 中的运算符

这只是我个人的看法,但我强烈建议您坚持第一种方法。第一个和单个元素的技巧可能会打扰任何未来的人 reader 或代码的维护者,因为它不是惯用的。

int (*a)[SIZE][SIZE]

a 声明为指向 SIZE 的指针 SIZE array of int - 假设 SIZE == 3,你会得到这样的东西:

   +---+          +---+---+---+
a: |   | -------> |   |   |   |
   +---+          +---+---+---+
                  |   |   |   |
                  +---+---+---+
                  |   |   |   |
                  +---+---+---+

(实际上,布局将是严格线性的,但我们现在将使用这种表示形式)。

要访问指向数组的任何元素,我们会写 (*a)[i][j] - 我们必须显式取消引用 a,因为我们不想索引到 a,我们想要索引到 a 指向 的内容。

请记住 a[i] 被定义为 *(a + i) - 给定一个地址 a,从该地址偏移 i 个元素(不是字节!)并推导结果。因此,(*a)[i][j] 等同于 a[0][i][j]

现在,如果a指向int的3x3数组,那么a + 1指向[=的下一个3x3数组 20=]:

   +---+          +---+---+---+
a: |   | -------> |   |   |   |
   +---+          +---+---+---+
                  |   |   |   |
                  +---+---+---+
                  |   |   |   |
                  +---+---+---+
a + 1: ---------> |   |   |   |
                  +---+---+---+
                  |   |   |   |
                  +---+---+---+
                  |   |   |   |
                  +---+---+---+

我们将访问为 (*(a + 1))[i][j],或简称为 a[1][i][j]

现在,为什么首先使用指向数组的指针?在这种情况下,我们将动态分配数组,如果 a) 在运行时之前我们不知道需要多少 SIZExSIZE 个数组,或者 b) 如果生成的数组太大而无法分配为 auto 变量,或者 c) 如果我们想根据需要扩展或缩小 SIZExSIZE 数组的数量。

这种分配多维数组的方法是如何工作的?让我们从分配 T:

N 元素数组开始
T *arr = malloc( sizeof *arr * N );

sizeof *arr 等同于 sizeof (T),因此我们为 T 类型的 N 对象预留 space。

现在让我们用数组类型替换TR [M]:

R (*arr)[M] = malloc( sizeof *arr * N );

sizeof *arr 等同于 sizeof (R [M]),因此我们为 N 类型的 R [M] 对象留出 space - IOW,N M - R 的元素数组。我们已经动态创建了 R a[M][N] 的等价物。

我们也可以这样写

R (*arr)[M] = malloc( sizeof (R) * M * N );

虽然我更喜欢使用 sizeof *arr;你很快就会明白为什么。

现在,我们可以将 R 替换为 另一个 数组类型,S [L]:

S (*arr)[L][M] = malloc( sizeof *arr * N );

sizeof *arr 等同于 sizeof (S [L][M]),因此我们为 N 类型 S [L][M] 或 [=41= 的对象分配了足够的 space ] LSM 个数组组成。我们已经动态创建了 S arr[L][M][N] 的等价物。

动态分配 1D、2D 和 3D 数组的语义完全相同 - 唯一不同的是类型。通过每次使用 sizeof *arr,我只需要跟踪我需要多少该类型的元素。