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



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

例如:

#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 ("%dn", (*a)[i][j]);
}
}
free (a);
return EXIT_SUCCESS;
}

这与计算索引然后执行指针艺术(例如*(a + (x * SIZE + y))或更简洁地a[x * SIZE + y])来访问元素。

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

除此之外,我不明白这是如何工作的。这个符号到底做了什么?是句法糖吗?它看起来与数组的动态堆栈分配相似(请参阅允许运行时没有动态分配的数组大小?作为这方面的一个例子),但显然这种分配发生在堆上。

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

编辑#1:

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

int (*a)[SIZE][SIZE]

通过int数组将a声明为指向SIZESIZE指针 - 假设SIZE == 3,你会得到这样的结果:

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

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

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

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

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

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

我们将作为(*(a + 1))[i][j]访问,或者干脆a[1][i][j].

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

这种分配多维数组的方法是如何工作的? 让我们首先分配一个N元素数组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,NMR的元素数组。 我们动态创建了等效的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对象分配了足够的空间,或者通过S数组MNL。 我们动态创建了等效的S arr[L][M][N]

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

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

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

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

那么,那是什么惊人的int (*a)[SIZE][SIZE] = malloc (sizeof (int) * SIZE * SIZE);呢?

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

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

>int (*a)[SIZE][SIZE]是指向int[SIZE][SIZE]类型的数组的数组指针。这是一种特殊的指针,用于指向整个数组,但在其他方面的工作方式与任何其他指针类似。因此,当你写(*a)[i][j]时,你会说"给我指针(2D 数组)的内容,然后在这些内容中给我项目编号 [i][j]"。

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

数组指针、上述技巧以及如何使用它们将 2D 数组动态分配为单个内存块,都在我正确分配多维数组的回答中得到解决。

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)的偏移量。你可以通过使用*(&a + 3)甚至使用3[a]来访问相同的元素,因为获取地址并添加偏移量与获取偏移量并添加地址相同。

因此,当您使用a[2][3]时,编译器的操作与上述完全相同,只是维度更多。因此,您无需执行a[x * SIZE + y],因为这正是编译器在您执行a[x][y]时为您执行的操作。

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

我希望我的解释是清楚的。

相关内容

  • 没有找到相关文章

最新更新