提问人:Luke Dunn 提问时间:9/14/2021 最后编辑:dbushLuke Dunn 更新时间:9/15/2021 访问量:344
“array[x][y]”总是用指针算术计算吗?
Is "array[x][y]" always calculated with pointer arithmetic?
问:
根据我对 C 语言的理解:
当你声明一个 2D 数组时,比如 ,程序会得到一块内存 (x*y) 的长。int array[x][y] = {0};
int
当你对一个 2D 数组进行 malloc 时,例如:
int ** array = malloc(sizeof(int*) * x);
for(int i=0;i<x;i++) {array[i] = malloc(sizeof(int) * y)};
程序得到一块内存 (x 's) + (x*y) 的长。int*
int
我遇到的问题是:当你以后输入时,会发生什么?程序是否总是将其视为指针算术(这表明编译器在声明数组时会为您创建一个指针数组)?或者编译器是否根据您创建数组的方式以不同的方式处理该语句?array[5][0]
编辑:将“int * array”更改为“int ** array”
答:
C 在其前身 B 的基础上增加了一个重大创新,即数组的基指针没有存储的位置,这意味着该名称不是命名指向第一个元素的指针,而是命名数组本身。
无论数组是基本类型的数组、用户定义类型(结构、联合)还是数组类型的数组,都不会改变任何内容。
因此,是的,数组衰减为指针,该指针用于指针算术(具有讽刺意味的是,数组索引是指针算术加取消引用的糖),产生一个数组类型的数组元素,在指针衰减后又用于指针算术。
所有计算出的中间值就是这样,不需要存储在其他任何地方。
你的第二个例子实际上不是一个多维数组,而是一个指针数组,一个不同的野兽,尽管使用相同的语法进行访问。
评论
array[3][0]
数组索引等同于指针算术加上取消引用。具体来说,与E1[E2]
*((E1) + (E2))
对于 2D 数组或指针到指针,这种情况会发生两次。鉴于您的示例,这与 相同。array[5][0]
*(array[5] + 0)
*(*(array + 5) + 0)
至于指针算术方面会发生什么,首先让我们看一下 2D 数组的情况。在表达式中,被转换为指向其第一个元素的指针,因此具有类型。因此,将 5 添加到此指针会将生成的指针向上移动它所指向的大小(即 a )乘以 5。array + 5
array
int(*)[y]
int[y]
对于指针到指针,将生成的点向上移动它所指向的大小(即 )乘以 5。array + 5
int *
所以它是完全相同的表达式,但指针算术是不同的,因为所指向的内容是不同的。
评论
int(*)[y]
int[y]
您的代码无效 - 它应该是:
int **array = malloc(sizeof(int*) * x);
//or better
int **array = malloc(sizeof(*array) * x);
但是这样,您就不会分配 2D 数组,而只分配指针数组。
在这种情况下,程序必须首先取消引用指针数组。然后使用此指针,第二个索引将引用 int 值。它不是很有效,因为需要至少从内存中读取两次。
2D 数组被分配为一个内存块。元素在内存中的位置由程序计算,无需从内存中额外读取。https://godbolt.org/z/5adjqxeKP
要动态分配 2d 数组,您需要使用指向数组的指针:
int (*array)[x] = malloc(sizeof(*array) * y);
int (*array1)[x][Y] = malloc(sizeof(*array));
并引用:
array[3][2] = 5;
(*array1)[4][5] = 6;
C 使用操作数的类型来决定如何计算它们。
如果 是 ,则在 :array
int [x][y]
array[5][0]
array
是一个数组,因此它会自动转换为指向其初始元素的指针。我们将此指针的值称为 p。- 然后指索引为 5 的元素。
p[5]
array
p[5]
也是一个数组,因此它被转换为指向其初始元素的指针。我们将此指针的值称为 q。- 然后引用索引为 0 的元素。
q[0]
p[5]
- 因此,我们有元素 5 的元素 0。
array
如果 是 ,则在 :array
int **
array[5][0]
array
是一个非数组类型的左值,因此它会自动转换为存储在其中的值。请注意,当 is 数组类型时,指向其第一个元素的指针是通过知道数组的存储位置来计算的。在这里,没有数组类型,值是从内存中获取的。同样,我们称加载的值为 p。array
array
- 然后引用索引为 5 的元素,假设有一个元素数组存储在 p 点的位置。
p[5]
- 因为 的类型是 ,它所指向的事物具有类型。所以有类型.
array
int **
int *
p[5]
int *
p[5]
是一个非数组类型的左值,因此它会自动转换为存储在其中的值。同样,这是通过从内存中加载存储的值来完成的。我们称加载的值为 q。q[0]
指 q 指向的元素。因此,我们在距离点的偏移量为 0 处有元素,并且是距离点偏移量为 5 的元素。p[5]
p[5]
array
所以计算方式不同。当它是数组的数组时,内存地址是根据 的基址计算的。当它是 时,内存地址是通过从内存中加载指针来计算的。array[5][0]
array
int **
注意
lvalue 转换是如此自动和无处不在,以至于我们通常不会考虑它。在 中,引用对象,这些对象的值会自动加载并在表达式中使用。这称为左值转换。 还引用一个对象,但它不会转换为其值,因为赋值运算符的左操作数存在异常。(当左值是 、一元 、 或 的左操作数时,也不会进行转换。x = y + z;
y
z
x
sizeof
&
++
--
.
在某些语言中,没有自动左值转换,您必须显式加载值。例如,在 BLISS 中,您必须编写 ,其中 指示加载值。x = .y + .z
.
图片可能会有所帮助。由于篇幅所限,我们假设 2 和 2。鉴于声明x
y
int arr[2][2];
我们在内存中得到这个:
int
+–––+
arr: | | arr[0][0]
+–––+
| | arr[0][1]
+–––+
| | arr[1][0]
+–—-+
| | arr[1][1]
+–––+
请注意,没有为任何指针留出空间 - 没有与数组元素本身分开的对象。arr
对于代码
int **arr = malloc( 2 * sizeof *arr );
for ( size_t i = 0; i < 2; i++ )
arr[i] = malloc( 2 * sizeof *arr[i] );
我们得到这个:
int ** int * int
+–––+ +–––+ +–––+
arr: | | -–> | | arr[0] ––––> | | arr[0][0]
+–––+ +–––+ +–––+
| | arr[1] ––+ | | arr[0][1]
+–––+ | +–––+
|
| +–––+
+–> | | arr[1][0]
+–––+
| | arr[1][1]
+–––+
在本例中,您有三个指针 - 指向一系列指针,每个指针指向一个 .arr
int
那么,如何评估每种方法呢?arr[x][y]
请记住,表达式定义为 - 给定一个地址,从该地址偏移元素(不是字节!),并取消引用结果。a[i]
*(a + i)
a
i
arr[i][j] == *(arr[i] + j) == *(*(arr + i) + j)
如果 是 的 2D 数组或指向指向 的指针的指针,则计算结果完全相同。arr
int
int
在第二种情况下,事情非常明显 - 我们正在处理一堆明确的指针。 显式存储 的地址 ,显式存储 的地址 ,等等。所以这是完全合乎逻辑的。arr
arr[0]
arr[0]
arr[0][0]
arr[i] == *(arr + i)
arr[i][j] == *(*(arr + i) + j)
但是第一种情况呢?任何位置都不会显式存储任何指针。 不存储的地址(没有单独的,这意味着没有任何东西可以存储的地址)。那么如何评价呢?arr
arr[0]
arr[0]
arr[0][0]
arr[i][j]
*(*(arr + i) + j)
像这样 - 除非它是 或 一元运算符的操作数,或者是用于初始化数组的字符串文字,类型为“N 元素数组”的表达式将被转换为或“衰减”类型的表达式“指针”,并且表达式的值将是数组第一个元素的地址。sizeof
&
char
T
T
当编译器在代码中看到该表达式时,除非该表达式是 的操作数或一元数,否则它会将该表达式替换为指针,并且该指针的值是数组第一个元素的地址。同样,表达式也替换为指针,该指针值是 的地址。请注意,在这种情况下,衰减为类型“指针指向 2 元素数组”(),而不是“指针指针指向”。arr
sizeof
&
arr[i]
arr[i][0]
arr
int
int (*)[2]
int
Expression Type Decays to Value
---------- ---- --------- -----
arr int [2][2] int (*)[2] Same as &arr[0]
*arr int [2] int * Same as arr[0]
arr[i] int [2] int * Same as &arr[i][0]
*arr[i] int n/a Same as arr[0][0]
arr[i][j] int n/a
&arr int (*)[2][2] n/a Address of array object
&arr[i] int (*)[2] n/a Address of the i'th subarray
因此,我们可以这样考虑被评估:arr[i][j]
*(*(&arr[0] + i) + j)
数组的地址与其第一个元素的地址相同 - 表达式 、 、 和 都产生相同的地址值,但表达式的类型不同 - 、 、 、 和(这可能会影响指针值的表示方式 - 可能具有不同的表示形式,尽管在您可能遇到的任何系统上都不是这种情况)。&arr
arr
&arr[0]
arr[0]
&arr[0][0]
int (*)[2][2]
int (*)[2]
int (*)[2]
int [2] => int *
int *
int (*)[2]
int *
记住指针算术的工作原理 - 如果指向 类型的对象,则生成该类型的下一个对象的地址。如果指向 的 2 元素数组,则生成下一个 2 元素数组 的地址。回到我们的第一张图片,但现在有一些额外的表达:p
T
p + 1
arr
int
arr + 1
int
int int (*)[2][2] int (*)[2] int *
+–––+ ------------- ---------- -----
| | arr[0][0] <-- &arr arr *arr + 0 (arr[0] + 0)
+–––+
| | arr[0][1] <-- *arr + 1 (arr[0] + 1)
+–––+
| | arr[1][0] <-- arr + 1 *(arr + 1) + 0 (arr[1] + 0)
+–—-+
| | arr[1][1] <-- *(arr + 1) + 1 (arr[1] + 1)
+–––+
同样,每当编译器看到表达式时,它都会将其替换为 的值,并使用指针算术进行下标。arr
&arr[0]
井
int variable[5][10];
将变量声明为包含 5 个元素的单个数组,这些元素都是类型(10 个元素的数组),其单元格大小(假设 int 为 4 个字节)为 10*4 = 40 字节。这意味着这是第四个元素,位于变量加 3x40 = 120 字节的地址处。int[10]
variable[3]
在那里,有一个 10 秒的数组,如果您尝试访问第三个元素 (),则必须添加 2 倍,即 8 个字节。int
variable[3][2]
sizeof(int)
所以,是的,对于 2D 数组,指针算术的工作方式与 1D 数组的工作方式相同。但是当你用 malloc 动态分配一些东西时,你需要知道编译器管理数组类型(这些类型是不同的,并且是不同的类型,并且 * C 中的所有类型都必须在编译时知道——这是 C 静态类型特性的一部分——)sometype[n][m]
n
m
如果您想使用某种方式在 C 中指定动态数组(以符号访问的数组),其中空间是动态分配的,并且只分配所需的空间......您必须使用指针来解决所有中间访问问题,因为运行时没有有关数组维度的信息,因为您使用的是指针,而不是数组类型。您不能声明数组类型并使其不完整...为此,您可以使用指针。即使在参数声明中,当你声明数组类型的参数时,它也会自动转换为指针......因为你不能按值传递数组,而且 C 语言不检查数组边界。[a][b]...[z]
所以指针算术本质上是一维的,你把指针用于动态数组......编译器不知道单元格(或行)的大小...因此,它不能假设每个行单元格的大小为 26 个整数或更小......您需要放置一个指向数据开头的指针(解决从前到后的计算),并且必须创建 AXBXC...。XY y 指针(双指针、三指针、四指针等),直到分配完整数组。
如何做到这一点(例如,处理 by 的 2D 矩阵)?n
m
double **m = malloc(n * sizeof(*m)); /* size of n pointers to double */
assert(m != NULL);
for (int row = 0; row < n; row++) {
m[row] = malloc(m * sizeof m[row][0]);
assert(m[row] != NULL);
}
这将分配一个伪矩阵,其中将是一个有效的元素并且可以访问,但不是元素数组,而是指向元素数组的指针。m[3][2]
double
m[2]
m
m
评论
(x*y) int's
long。”应该是“程序获得一个内存块 (x int*'s) 长,
x
个内存块 (yint's
) 长。sizeof(TYPE)
sizeof *pointer
int ** array = malloc(sizeof(int*) * x);
不是分配 2D 数组,也不是分配 2D 数组,而是分配双指针。指向的值必须是指针,而不是 s,这是 2D 数组所要求的。int **
int
int