【问题标题】:why is int ** x not the same as int x[][]?为什么 int ** x 与 int x[][] 不一样?
【发布时间】:2020-10-25 03:35:04
【问题描述】:

这可能是一个愚蠢的问题,但我不明白为什么会出现此错误:

void func (int ** array) { }
 
int main (void) {
    int array[5][5] = { };
    func(array);
}


 warning: passing argument 1 of 'func' from incompatible pointer type [-Wincompatible-pointer-types]
      func(array);
           ^~~~~
    note: expected 'int **' but argument is of type 'int (*)[5]'
     void func (int ** array) { }
            ~~~~~~~^~~~~

【问题讨论】:

  • 您可能会喜欢comp.lang.c faq 的第 6 节:简而言之:数组不是指针,指针不是数组。
  • 另外,函数中的int ** array不知道int array[5][5]每一行的宽度。
  • 为什么需要宽度?
  • 因为为了访问array[3][2],处理器需要计算(3*width + 2) * sizeof(int)。如果您将函数声明为void func(int (*array)[5]),则编译器拥有进行计算所需的信息。如果编译器支持variable length arrays,您可以将函数声明为void func(int rows, int cols, int array[rows][cols])。这将允许代码使用任何大小的数组。
  • 首先:欢迎来到SO!对于您的问题:也许typedef 可以使其更具可读性。检查我的答案。

标签: arrays c pointers multidimensional-array syntax-error


【解决方案1】:

考虑一个函数void foo(int **p)。给出的p 包含一些地址。在那个地址,有一个int *。如果foo 从该地址加载数据并将其用作加载地址,它将加载int。

现在考虑一个数组int a[5][5]。假设我们获取这个数组的地址(或者,在某种程度上,它的第一个元素的地址)并将它传递给foo。然后foo 有一个p,其中包含a 的地址。在那个地址,没有int *。在那个地址,有 25 个int 值,以 5 个为一组排列成 5 组。

所以,int [5][5] 不是int **。并且它的第一个元素的地址不是int **,它的第一个元素也不是int *。

这就是为什么你不能传递int [][] 来代替int **。

相比之下,考虑一个函数void foo(int *p)。给定的p 这个函数包含一些地址。在那个地址,有一个int。如果我们有一个数组int a[5],并且我们将它的第一个元素的地址传递给foo,那么foo 会收到一个包含int 的地址的p。因此,已转换为其第一个元素的地址的int [] 是int *。

这些自动数组转换仅在数组的第一个维度上起作用。如果你有一个int a[5][6][7],自动转换到它的第一个元素会给你一个指向int [6][7]的指针,它是一个int (*)[6][7]。指针指向内存中的整数组。它不指向指针。

数组到指针的自动转换不会改变内存中的实际内容。所以它不能改变指向的东西的类型:我们可以在int [][]中指向int,但是我们不能指向指向它们的指针,因为内存中没有这样的指针。

【讨论】:

  • 所以如果我理解正确的话:int x[5][5] 不是一个由 5 个指针组成的数组,每个指针指向 5 个整数,而是一个由 25 个整数组成的数组?
  • 与int x[5][5]; 你有x 是5 个“事物”的数组,即:x[0] 是其中一个“事物”,x[1] 是另一个“事物”,等等。每个“事物”是一个由 5 个ints 组成的数组。所以你有一个数组数组,而不是指针数组。
【解决方案2】:

int ** 类型表示指向int 的双指针,但这里有一个指针数组。因此,您的函数声明应该是

    void func (int (*array)[]) { }

指向int 的双指针的内部功能可以表示如下:

+-------+     +------+     +-----+
| int** | --> | int* | --> | int |
+-------+     +------+     +-----+

每个方块应该代表一个内存位置,其中包含类型。相比之下,int (*array)[] 的表示形式如下:

                      +-------------+
array ->  array[0] -> | array[0][0] |
                      +-------------+
                      | array[0][1] |
                      +-------------+
                      |     ...     |
                      +-------------+
          array[1] -> | array[1][0] |
                      +-------------+
                            ...      

每个方块显示如何访问相应的值。请注意,只有最右边的值在内存中。这里不需要指针,因为可以直接计算数组元素的地址。因此

printf("%ld\n", sizeof(array)/sizeof(int)); // Outputs 25

表示已为 25 个值保留了内存空间。所以在内部它与声明int array[25] 相同,但具有不同的类型和对数组元素的访问权限。

【讨论】:

  • 好的,但是为什么指针数组与指向指针的指针不同?因为数组是指针
  • 我的意思是 int ** a 和 int a [][] 在较低级别有什么区别?
  • @moutonlapin28 因为数组是指针 不,不! NO! 数组不是指针,指针也不是数组。指针是一个保存地址的变量,该地址甚至可能引用一些有用的东西。数组是一块存在的内存,所以它有一个地址。数组通过第一个元素的地址传递给函数。其中int array[5][5] 是int (*)[5] 或指向5 个int 值数组的指针。
【解决方案3】:

如果这是声明的一部分:

type **var;

指的是指向指针的指针,即可以保存另一个变量的地址的变量,该变量也是指向type类型的var的指针。

但不是这种情况:

type *var[N];

这向编译器声明这是指向变量var 的N 包含数组元素的指针。

作为一种解引用:

**var 是双重取消引用。仅当变量是地址的地址时,结果表达式才会是存储在*var 中的地址的左值,类型为type。


您正在尝试将 指向指针的指针 类型传递给函数func() 接受的指向数组的指针 类型。这就是错误的原因。

【讨论】:

    【解决方案4】:

    一些注释来澄清一些事情:

    • 即使指针和数组通常可以以相同的方式使用并且看起来相同,但它们实际上是不同的。
    • 数组指定存储(忽略不完整的数组,例如此处的a[],但它们仍需要在其他地方定义存储)。
    • 指针未定义存储(它指向)。
    • 编译器将表达式中的数组名视为指向第一个元素的指针。
    • 函数参数声明中的数组名也是如此。
    • 下标始终等价于指针的偏移量。
    • 在将参数传递给函数时,可能会发生隐式转换(请记住这一点),但有时这会失败,这就是编译器所说的 (int ** != int *[])。 您可以进行显式转换,但这会很麻烦并且需要仔细编程。更好地修复函数原型。

    使用int **array,您只需定义一个指向int 的句柄(指向指针的指针)。但正如编译器所说,您正在传递int *array[5],它是一个由 5 个指向 int 的指针组成的数组。您需要更改函数声明以将指针传递给 int 数组。因为否则func 将无法访问您传递的该数组的元素。

    这意味着: 在“构建” tyoe 声明时,您在第一阶段拥有一个数组int array[],并且需要在第二阶段将其更改为指向该数组的指针:int ... *array ... [](没有绘图无法更好地解释)这将最后是int (*array)[]。

    如果有助于澄清,请使用typedef:

    typedef int (*ah)[];
    void func (ah x) { }
    

    或者(更好):

    typedef int a[];
    void func (a *x) { }
    

    这里可以清楚的看到x到底是哪种类型的指针。

    【讨论】:

      【解决方案5】:

      除非它是sizeof、_Alignof 或一元& 运算符的操作数,或者是用于在声明中初始化字符数组的字符串文字,表达式 “T 的 N 元素数组”类型的表达式将被转换(“衰减”)“指向 T”的类型的表达式,表达式的值将是数组的第一个元素。

      当您将表达式 array 传递给func 时,它会从类型“5 元素数组int”“衰减”到“指向5 元素的指针” int" (int (*)[5]) 的数组。所以你的函数原型需要是

      void func( int (*array)[5] )
      

      您可以像任何二维数组一样对它进行索引。

      编辑

      来自cmets:

      但是低级有什么区别呢?

      内存的结构不同。真正的 2D 数组的所有行都是连续布局的,并且所有行都具有相同数量的元素。给定定义

      int a[2][2] = {{1, 2}, {3, 4}};
      

      它在内存中的布局是这样的(地址只是为了说明):

         +–––+
         | 1 | a[0][0] (0x8000)
         + – + 
         | 2 | a[0][1] (0x8004)
         +–––+
         | 3 | a[1][0] (0x8008)
         + - +
         | 4 | a[1][1] (0x800C)
         +–––+
      

      指针算法考虑了指向类型的大小 - 给定一个指针 p,表达式 p + 1 产生指向类型的下一个对象的地址,而不是下一个字节。

      这就是数组下标的工作原理——表达式a[i] 完全等同于*(a + i)。给定起始地址a,从该地址偏移i 元素并取消引用结果。在a 引用数组的情况下,它“衰减”为指向第一个元素的指针。

      当我们写作时

      x = a[1][1];
      

      表达式a“衰减”为int (*)[2],其值为a (0x8000) 的第一个元素的地址。 a[1] 被评估为 *(a + 1)。 a + 1 产生 int 的第二个 2 元素数组的地址 (0x8008) 在 a 之后。

      由于表达式a 的类型为int (*)[2],因此表达式*a 的类型为int [2]。由于*a == *(a + 0),表达式a[0] 的类型为int [2],并且通过扩展任何a[i] 的类型为int [2]。

      由于表达式a[1]的类型为int [2],它衰减为int *类型,其值为a[1](0x8008)的第一个元素的地址。 a[1][1] 被评估为 *(a[1] + 1)。 a[1] + 1 产生在 a[1] 之后的第二个 int 对象的地址。

      int ** OTOH 通常用于实现如下所示的结构:

         int **            int *               int
         +–––+             +–––+               +–––+
      a: | +-+–––––> a[0]: | +-+––––> a[0][0]: | 1 | 
         +–––+             +–––+               +–––+
                     a[1]: | +–+––+   a[0][1]: | 2 | 
                           +-––+  |            +–––+
                                  |             ...
                                  |            +–––+
                                  +–> a[1][0]: | 3 | 
                                               +–––+
                                      a[1][1]: | 4 | 
                                               +–––+
      

      数组的“行”在内存中不是连续的,它们甚至不必是相同的长度。这种结构通常分配为

      int **a = malloc( sizeof *a * 2 );
      if ( a )
      {
        for ( int i = 0; i < 2; i++ )
        {
          a[i] = malloc( sizeof *a[i] * 2 );
          if ( a[i] )
          {
            a[i][0] = 2 * i;
            a[i][1] = 2 * i + 1;
          }
        }
      }
      

      在这种情况下,a 明确是一个指针,每个 a[i] 明确是一个指针。没有从数组类型到指针类型的隐式转换。您可以使用与二维数组相同的符号对该结构进行索引,但底层结构完全不同。

      【讨论】:

      • 但是低层有什么区别呢?
      • 只用双指针访问任何值都是可能的:如果我们有 int ** x,我们需要 x[3][4],我们去 *x(地址数组),我们做 +3 所以我们的意思是 *(x + 3),然后我们去 *(*(x + 3)),一个整数数组,我们做 + 4,*(*(x + 3) + 4) 我们得到了我们想要的 int。
      猜你喜欢
      • 2010-09-11
      • 1970-01-01
      • 2019-05-13
      • 2012-02-15
      • 2012-10-04
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      • 2019-09-06
      相关资源
      最近更新 更多