【问题标题】:How does (*prt)[N][N] work when dealing with heap allocation memory?(*prt)[N][N] 在处理堆分配内存时如何工作?
【发布时间】:2018-10-19 14:23:27
【问题描述】:

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

例如:

#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? 作为其中的一个示例),但显然这种分配发生在堆上。

我已经查找了有关指针的这种表示法/声明的更多信息,但除了即将出现的术语 元素类型 之外找不到更多信息 - 但我不确定这是否相关.

编辑#1:

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

【问题讨论】:

    标签: c arrays multidimensional-array malloc c99


    【解决方案1】:

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

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

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

    int a[SIZE][SIZE];
    

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

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

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

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

    我希望我的解释很清楚。

    【讨论】:

    • 需要堆分配可能有多种原因。例如,在大多数操作系统中,您应该避免在堆栈上分配大对象,以防止堆栈溢出。话虽如此,您的回答并没有真正解释问题中的代码是做什么的。
    • @Lundin 我认为这不是问题所在。在我看来,他只是试图访问一个多维数组而不进行指针运算,这对于堆栈和堆分配的数组都是完全可能的。
    • 这个答案没有抓住重点。问题中的代码只是一个示例。更清楚地说,它应该从某个动态值中获取数组大小,而不是使用预处理器定义的常量。但其目的是解决动态分配内存并且在编译时不知道大小的情况。此外,严格来说,C 与“内存地址偏移”一起工作并不正确。 C 标准使用抽象 mdoel 定义行为,实现可以使用其他方式来完成所需的结果。
    • @EricPostpischil 你说得对,我没抓住重点。我编辑了我的问题以更好地解决它。但是,您对地址偏移量的事情是错误的,指针根据定义是对内存地址的引用,并且下标运算符的 C 标准定义器行为如下:“下标运算符 [] 的定义是 E1[E2] 是等同于 (*((E1)+(E2)))。"
    • @dieortin ...其中 *(E1 + E2) 的结果是取消引用的指针,而不是地址。如果 E1 或 E2 是地址,则代码显然会中断,然后需要对字节而不是指向类型进行算术运算。
    【解决方案2】:

    int (*a)[SIZE][SIZE] 是一个数组指针,指向int[SIZE][SIZE] 类型的数组。这是一种特殊的指针,用于指向整个数组,但在其他方面与任何其他指针一样工作。所以当你写 (*a)[i][j] 你说“给我指针的内容(二维数组),然后在这些内容中给我项目编号 [i][j]”。

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

    数组指针,上述技巧,以及如何使用它们将二维数组动态分配为一块内存,都在我对Correctly allocating multi-dimensional arrays 的回答中得到解决。

    【讨论】:

    • 实际上并没有“数组指针”这样的东西。所有的指针都只是内存地址,它的类型可以让你做一些事情,比如使用索引而不是手动进行指针运算。
    • 另外,没有办法“指向整个二维数组”,因为指针只是内存地址。最接近这样做的就是指向第一个元素,C 就是这样做的。
    • @dieortin 相反,C 语言中没有内存地址这样的东西,标准缺少这个概念。如果您在链接的答案中向下滚动,您会找到一个如何使用数组指针进行算术运算的示例。您可以将数组指针视为指向整个对象,因为 C 不允许使用诸如 int (*a)[1]; int (*b)[2] = a; 之类的代码。
    【解决方案3】:

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

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

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

    那么int (*a)[SIZE][SIZE] = malloc (sizeof (int) * SIZE * SIZE); 有什么了不起的?

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

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

    【讨论】:

    • 其实我会说int (*a)[SIZE][SIZE]是最正确的声明,但是使用起来不太实用。
    【解决方案4】:
    int (*a)[SIZE][SIZE]
    

    a 声明为指向SIZESIZE 数组的SIZE 的指针 - 假设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 指向int下一个3x3 数组

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

    我们将以(*(a + 1))[i][j] 或简单的a[1][i][j] 访问。

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

    这种分配多维数组的方法是如何工作的?让我们先分配一个N-element 数组T

    T *arr = malloc( sizeof *arr * N );
    

    sizeof *arr 等价于sizeof (T),因此我们为N 类型为T 的对象留出了空间。

    现在让我们将T 替换为数组类型R [M]

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

    sizeof *arr 等价于sizeof (R [M]),因此我们为R [M] 类型的N 对象留出空间 - 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]),因此我们为S [L][M] 类型的N 对象或N L MS 数组分配了足够的空间。我们已经动态地创建了 S arr[L][M][N] 的等价物。

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

    【讨论】:

    • 谢谢,这是一个非常明确的答案 - 通过将我的类型从 T [M][N] 更改为 T [M] 并使用 sizeof (*arr) 进行分配,我不再需要取消引用指针 - 确实使代码成为更容易理解/整洁。
    猜你喜欢
    • 1970-01-01
    • 2017-08-11
    • 1970-01-01
    • 2019-01-24
    • 1970-01-01
    • 1970-01-01
    • 2021-11-13
    • 1970-01-01
    • 1970-01-01
    相关资源
    最近更新 更多