“array[x][y]”总是用指针算术计算吗?

Is "array[x][y]" always calculated with pointer arithmetic?

提问人:Luke Dunn 提问时间:9/14/2021 最后编辑:dbushLuke Dunn 更新时间:9/15/2021 访问量:344

问:

根据我对 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 多维数组 malloc 指针算术

评论

0赞 user3386109 9/14/2021
“程序得到一块内存 (x int*'s) + (x*y) int's long。”应该是“程序获得一个内存块 (x int*'s) 长,x 个内存块 (y int's) 长。
0赞 Deduplicator 9/14/2021
顺便说一句,避免 , prefer ,后者不太容易出错,并将大小与目标耦合,而不是一些手动解析的类型,这可能是错误的或过时的。sizeof(TYPE)sizeof *pointer
0赞 Luis Colorado 9/15/2021
int ** array = malloc(sizeof(int*) * x);不是分配 2D 数组,也不是分配 2D 数组,而是分配双指针。指向的值必须是指针,而不是 s,这是 2D 数组所要求的。int **intint

答:

2赞 Deduplicator 9/14/2021 #1

C 在其前身 B 的基础上增加了一个重大创新,即数组的基指针没有存储的位置,这意味着该名称不是命名指向第一个元素的指针,而是命名数组本身。

无论数组是基本类型的数组、用户定义类型(结构、联合)还是数组类型的数组,都不会改变任何内容。

因此,是的,数组衰减为指针,该指针用于指针算术(具有讽刺意味的是,数组索引是指针算术加取消引用的糖),产生一个数组类型的数组元素,在指针衰减后又用于指针算术。

所有计算出的中间值就是这样,不需要存储在其他任何地方。

你的第二个例子实际上不是一个多维数组,而是一个指针数组,一个不同的野兽,尽管使用相同的语法进行访问。

评论

0赞 Luke Dunn 9/14/2021
所以你是说指针数组和“真正的”多维数组之间除了它们的存储方式之外还有一个主要区别?
1赞 Deduplicator 9/14/2021
是的。多维数组实际上是一个类型的数组(数组)+。虽然使用相同的语法访问指针数组,但底层机制是不同的。前者使用指针算术和数组衰减,另一种指针算术和读取链指针。
0赞 Luke Dunn 9/15/2021
因此,当有人键入时,可以根据数组的存储方式对其进行不同的处理吗?array[3][0]
0赞 Deduplicator 9/17/2021
@LukeDunn 取决于数组的类型。如果它是类型的数组的数组、指向类型的指针数组、指向类型的指针的指针,甚至是指向类型的数组的指针。
1赞 dbush 9/14/2021 #2

数组索引等同于指针算术加上取消引用。具体来说,与E1[E2]*((E1) + (E2))

对于 2D 数组或指针到指针,这种情况会发生两次。鉴于您的示例,这与 相同。array[5][0]*(array[5] + 0)*(*(array + 5) + 0)

至于指针算术方面会发生什么,首先让我们看一下 2D 数组的情况。在表达式中,被转换为指向其第一个元素的指针,因此具有类型。因此,将 5 添加到此指针会将生成的指针向上移动它所指向的大小(即 a )乘以 5。array + 5arrayint(*)[y]int[y]

对于指针到指针,将生成的点向上移动所指向的大小(即 )乘以 5。array + 5int *

所以它是完全相同的表达式,但指针算术是不同的,因为所指向的内容是不同的。

评论

0赞 Ian Abbott 9/14/2021
实际上,和.int(*)[y]int[y]
1赞 0___________ 9/14/2021 #3

您的代码无效 - 它应该是:

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;
2赞 Eric Postpischil 9/14/2021 #4

C 使用操作数的类型来决定如何计算它们。

如果 是 ,则在 :arrayint [x][y]array[5][0]

  • array是一个数组,因此它会自动转换为指向其初始元素的指针。我们将此指针的值称为 p
  • 然后指索引为 5 的元素。p[5]array
  • p[5]也是一个数组,因此它被转换为指向其初始元素的指针。我们将此指针的值称为 q
  • 然后引用索引为 0 的元素。q[0]p[5]
  • 因此,我们有元素 5 的元素 0。array

如果 是 ,则在 :arrayint **array[5][0]

  • array是一个非数组类型的左值,因此它会自动转换为存储在其中的值。请注意,当 is 数组类型时,指向其第一个元素的指针是通过知道数组的存储位置来计算的。在这里,没有数组类型,值是从内存中获取的。同样,我们称加载的值为 parrayarray
  • 然后引用索引为 5 的元素,假设有一个元素数组存储在 p 点的位置。p[5]
  • 因为 的类型是 ,它所指向的事物具有类型。所以有类型.arrayint **int *p[5]int *
  • p[5]是一个非数组类型的左值,因此它会自动转换为存储在其中的值。同样,这是通过从内存中加载存储的值来完成的。我们称加载的值为 q
  • q[0]q 指向的元素。因此,我们在距离点的偏移量为 0 处有元素,并且是距离点偏移量为 5 的元素。p[5]p[5]array

所以计算方式不同。当它是数组的数组时,内存地址是根据 的基址计算的。当它是 时,内存地址是通过从内存中加载指针来计算的。array[5][0]arrayint **

注意

lvalue 转换是如此自动和无处不在,以至于我们通常不会考虑它。在 中,引用对象,这些对象的值会自动加载并在表达式中使用。这称为左值转换。 还引用一个对象,但它不会转换为其值,因为赋值运算符的左操作数存在异常。(当左值是 、一元 、 或 的左操作数时,也不会进行转换。x = y + z;yzxsizeof&++--.

在某些语言中,没有自动左值转换,您必须显式加载值。例如,在 BLISS 中,您必须编写 ,其中 指示加载值。x = .y + .z.

1赞 John Bode 9/14/2021 #5

图片可能会有所帮助。由于篇幅所限,我们假设 2 和 2。鉴于声明xy

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]
                                  +–––+

在本例中,您有三个指针 - 指向一系列指针,每个指针指向一个 .arrint

那么,如何评估每种方法呢?arr[x][y]

请记住,表达式定义为 - 给定一个地址,从该地址偏移元素(不是字节!),并取消引用结果。a[i]*(a + i)ai

arr[i][j] == *(arr[i] + j) == *(*(arr + i) + j)

如果 是 的 2D 数组或指向指向 的指针的指针,则计算结果完全相同。arrintint

在第二种情况下,事情非常明显 - 我们正在处理一堆明确的指针。 显式存储 的地址 ,显式存储 的地址 ,等等。所以这是完全合乎逻辑的。arrarr[0]arr[0]arr[0][0]arr[i] == *(arr + i)arr[i][j] == *(*(arr + i) + j)

但是第一种情况呢?任何位置都不会显式存储任何指针。 不存储的地址(没有单独的,这意味着没有任何东西可以存储的地址)。那么如何评价呢?arrarr[0]arr[0]arr[0][0]arr[i][j]*(*(arr + i) + j)

像这样 - 除非它是 或 一元运算符的操作数,或者是用于初始化数组的字符串文字,类型为“N 元素数组”的表达式将被转换为或“衰减”类型的表达式“指针”,并且表达式的值将是数组第一个元素的地址。sizeof&charTT

当编译器在代码中看到该表达式时,除非该表达式是 的操作数或一元数,否则它会将该表达式替换为指针,并且该指针的值是数组第一个元素的地址。同样,表达式也替换为指针,该指针值是 的地址。请注意,在这种情况下,衰减为类型“指针指向 2 元素数组”(),而不是“指针指针指向”。arrsizeof&arr[i]arr[i][0]arrintint (*)[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)

数组的地址与其第一个元素的地址相同 - 表达式 、 、 和 都产生相同的地址值,但表达式的类型不同 - 、 、 、 和(这可能会影响指针值的表示方式 - 可能具有不同的表示形式,尽管在您可能遇到的任何系统上都不是这种情况)。&arrarr&arr[0]arr[0]&arr[0][0]int (*)[2][2]int (*)[2]int (*)[2]int [2] => int *int *int (*)[2]int *

记住指针算术的工作原理 - 如果指向 类型的对象,则生成该类型的下一个对象的地址。如果指向 的 2 元素数组,则生成下一个 2 元素数组 的地址。回到我们的第一张图片,但现在有一些额外的表达:pTp + 1arrintarr + 1int

 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]

0赞 Luis Colorado 9/15/2021 #6

int variable[5][10];

将变量声明为包含 5 个元素的单个数组,这些元素都是类型(10 个元素的数组),其单元格大小(假设 int 为 4 个字节)为 10*4 = 40 字节。这意味着这是第四个元素,位于变量加 3x40 = 120 字节的地址处。int[10]variable[3]

在那里,有一个 10 秒的数组,如果您尝试访问第三个元素 (),则必须添加 2 倍,即 8 个字节。intvariable[3][2]sizeof(int)

所以,是的,对于 2D 数组,指针算术的工作方式与 1D 数组的工作方式相同。但是当你用 malloc 动态分配一些东西时,你需要知道编译器管理数组类型(这些类型是不同的,并且是不同的类型,并且 * C 中的所有类型都必须在编译时知道——这是 C 静态类型特性的一部分——)sometype[n][m]nm

如果您想使用某种方式在 C 中指定动态数组(以符号访问的数组),其中空间是动态分配的,并且只分配所需的空间......您必须使用指针来解决所有中间访问问题,因为运行时没有有关数组维度的信息,因为您使用的是指针,而不是数组类型。您不能声明数组类型并使其不完整...为此,您可以使用指针。即使在参数声明中,当你声明数组类型的参数时,它也会自动转换为指针......因为你不能按值传递数组,而且 C 语言不检查数组边界。[a][b]...[z]

所以指针算术本质上是一维的,你把指针用于动态数组......编译器不知道单元格(或行)的大小...因此,它不能假设每个行单元格的大小为 26 个整数或更小......您需要放置一个指向数据开头的指针(解决从前到后的计算),并且必须创建 AXBXC...。XY y 指针(双指针、三指针、四指针等),直到分配完整数组。

如何做到这一点(例如,处理 by 的 2D 矩阵)?nm

    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]doublem[2]mm