今天,一位同事向我展示了一种声明二维数组的方法,我可以线性分配它,但仍然使用二维方括号([][]
)表示法来访问元素。
例如:
#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
声明为指向SIZE
SIZE
指针 - 假设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,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
对象分配了足够的空间,或者通过S
数组M
N
L
。 我们动态创建了等效的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]
时为您执行的操作。
编辑:正如一些人在评论中指出的那样,实际上指针不一定存储内存引用,尽管这绝对是最常见的实现。
我希望我的解释是清楚的。