【问题标题】:Is "array[x][y]" always calculated with pointer arithmetic?“array[x][y]”总是用指针算术计算吗?
【发布时间】:2021-11-09 03:52:55
【问题描述】:

根据我对 C 的理解:

当你声明一个像int array[x][y] = {0}; 这样的二维数组时,程序会获得一块内存 (x*y) int's long。

当你 malloc 一个二维数组时:

int ** array = malloc(sizeof(int*) * x);
for(int i=0;i<x;i++) {array[i] = malloc(sizeof(int) * y)};

程序获得一块内存 (x int*'s) + (x*y) int's long。

我遇到的问题是:当您稍后输入array[5][0] 时,会发生什么?程序是否总是将其视为指针算术(这表明编译器在您声明数组时会为您创建一个指针数组)?或者编译器是否会根据您创建数组的方式不同地处理该语句?

编辑:将“int * array”更改为“int ** array”

【问题讨论】:

  • "程序得到一块内存 (x int*'s) + (x*y) int's long。" 应该是 "程序得到一个内存块 (x int*'s) 长,x 内存块 (y int's) 长。"
  • 顺便说一句,避免sizeof(TYPE),更喜欢sizeof *pointer,后者不易出错并将大小与目标耦合,而不是手动解析的类型,这可能是错误的,或者过时了。
  • int ** array = malloc(sizeof(int*) * x); 没有分配二维数组,因为int ** 不是二维数组,而是双指针。指向的值必须是指针,而不是 ints,这是二维 int 数组所要求的。

标签: arrays c multidimensional-array malloc pointer-arithmetic


【解决方案1】:

C 对其前身 B 的最大创新是没有存储数组的基指针的位置,这意味着该名称没有命名指向第一个元素的指针,而是数组本身。

无论数组是基本类型、用户定义类型(struct、union)还是数组类型,都不会改变任何事情。

因此,是的,数组衰减为一个指针,该指针用于指针算术(讽刺的是数组索引,它是指针算术加解引用的糖),产生一个数组类型的数组元素,并且在指针之后-decay 依次用于指针算术。

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

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

【讨论】:

  • 所以你是说指针数组和“真正的”多维数组除了存储方式之外还有很大的区别?
  • 是的。多维数组实际上是(数组)+ 类型的数组。虽然使用相同的语法访问指针数组,但底层机制不同。前者使用指针运算和数组衰减,另一个使用指针运算和读取链接指针。
  • 所以当有人键入array[3][0] 时,可能会根据数组的存储方式而区别对待?
  • @LukeDunn 取决于数组的类型。如果是类型数组,类型指针数组,类型指针指针,甚至类型数组指针。
【解决方案2】:

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

如果arrayint [x][y],那么,在array[5][0]

  • array 是一个数组,因此它会自动转换为指向其初始元素的指针。我们将这个指针的值称为 p
  • 那么<i>p</i>[5]指的是array中索引为5的元素。
  • <i>p</i>[5] 也是一个数组,因此它被转换为指向其初始元素的指针。我们将这个指针的值称为 q
  • 那么<i>q</i>[0]指的是<i>p</i>[5]中索引为0的元素。
  • 因此,我们有 array 的元素 5 的元素 0。

如果arrayint **,那么,在array[5][0]

  • array 是一个非数组类型的左值,因此会自动转换为其中存储的值。请注意,当array 是数组类型时,指向其第一个元素的指针是根据知道数组的存储位置计算。这里,array 没有数组类型,值是从内存中获取的。同样,让我们​​调用加载的值 p
  • 然后<i>p</i>[5] 引用索引为 5 的元素,假设 p 指向的位置存储了一个元素数组。
  • 因为array的类型是int **,所以它指向的东西有int *的类型。所以<i>p</i>[5] 的类型为int *
  • <i>p</i>[5] 是一个非数组类型的左值,因此会自动转换为其中存储的值。同样,这是通过从内存中加载存储的值来完成的。我们将加载的值称为 q
  • <i>q</i>[0] 指的是 q 指向的元素。因此,我们的元素位于偏移量 0 处,距离 <i>p</i>[5] 点,<i>p</i>[5] 是偏移量 5 处的元素,距离 array 点。

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

注意

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

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

【讨论】:

    【解决方案3】:

    您的代码无效 - 应该是:

    int **array = malloc(sizeof(int*) * x);
    
    //or better
    
    int **array = malloc(sizeof(*array) * x);
    

    但是这样你就不会分配二维数组,只分配一个指针数组。

    在这种情况下,程序必须首先取消引用指针数组。然后使用这个指针,第二个索引将引用 int 值。它不是很有效,因为至少需要从内存中读取两次。

    二维数组被分配为一块内存。内存中元素的位置由程序计算,无需从内存中额外读取。 https://godbolt.org/z/5adjqxeKP

    要动态分配二维数组,您需要使用指向数组的指针:

    int (*array)[x] = malloc(sizeof(*array) * y);
    int (*array1)[x][Y] = malloc(sizeof(*array));
    

    和参考:

    array[3][2] = 5;
    
    (*array1)[4][5] = 6;
    

    【讨论】:

      【解决方案4】:

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

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

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

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

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

      【讨论】:

      • 其实int(*)[y]int[y]
      【解决方案5】:

      图片可能会有所帮助。为了篇幅,我们假设xy 是2。给定声明

      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)
      

      如果arrint 的二维数组或指向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)呢?

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

      当编译器在您的代码中看到 表达式 arr 时,除非该表达式是 sizeof 或一元 &amp; 的操作数,它会替换该表达式带有一个指针,该指针的值是数组第一个元素的地址。类似地,表达式arr[i]也被替换为一个指针,那个指针值就是arr[i][0]的地址。请注意,在这种情况下,arr 衰减为类型“指向 int2 元素数组 的指针”(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)
      

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

      记住指针算法的工作原理——如果p 指向T 类型的对象,那么p + 1 会产生该类型的下一个对象 的地址。如果arr 指向int 的2 元素数组,则arr + 1 产生int 的下一个2 元素数组 的地址。回到我们的第一张图片,但现在有一些额外的表达方式:

       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 时,它就会将其替换为 &amp;arr[0] 的值,并使用指针算法进行下标。

      【讨论】:

        【解决方案6】:

        嗯,

        int variable[5][10];
        

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

        还有一个由 10 个ints 组成的数组,如果您尝试访问第三个元素 (variable[3][2]),则必须添加 2xsizeof(int),即多 8 个字节。

        所以,是的,对于 2D 数组,指针算法的工作原理与 1D 数组一样。但是,当您使用 malloc 动态分配某些内容时,您需要知道编译器管理数组类型(类型为 sometype[n][m] 与不同的 nm 作为不同的类型,并且 *C 中的所有类型必须在编译时间——这是 C 的静态类型特性的一部分——)

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

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

        如何做到这一点(例如,通过m 处理n 的二维矩阵)?

            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 元素的数组,而是指向@987654338 数组的指针@元素。

        【讨论】:

          猜你喜欢
          • 1970-01-01
          • 1970-01-01
          • 2019-05-22
          • 1970-01-01
          • 1970-01-01
          • 1970-01-01
          • 1970-01-01
          • 1970-01-01
          • 2016-04-25
          相关资源
          最近更新 更多