int**和int[][]之间有什么区别?

19
如果以下赋值语句是有效的:
int a[2] = {1,2};
int* b = a;
那么这样做有什么问题呢?
int a[2][2]={1,2,3,4};
int** b = a;

C++ 报错,无法将 int[][] 转换为 int**。如果 int[]int* 相同,这两种类型有什么区别?


看看所有的答案,你会学到很多!(但也要花点时间);) - Roman Byshko
3
相关常见问题解答(FAQ) - fredoverflow
4个回答

31

放轻松,这只是编译器错误。数组非常棘手。这里是一个规则

类型为数组的变量的值会衰减(decay)为该数组的第一个元素的地址

你的第一段代码看起来像:

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

根据规则,如果a出现在赋值语句的右侧,则会衰减为元素0的地址,这就是它为什么具有int *类型的原因。这将带您到

int *b = a;
在第二个代码片段中,你实际上有的是一个数组嵌套另一个数组。(顺便说一下,为了更明确起见,我稍微改了一下你的代码。)
int a[2][2]={{1,2},{3,4}};

这次a将会退化为指向一个包含两个整数的数组的指针!因此,如果你想要将a赋值给某个东西,你需要确保这个东西有相同的类型。

int (*b)[2] = a; //Huh!

(这种语法可能会让您感到惊讶,但请思考一下我们写的 int *b[2]; 。明白了吗?b 将是一个指向整数的指针数组!这不是我们想要的……)

你可以在这里停止阅读,但你也可以继续看下去,因为我没有告诉你全部真相。我提到的规则有三个例外...

如果:

  1. 数组是sizeof的操作数
  2. 数组是&的操作数
  3. 数组是字符数组的文本字符串初始化器

那么数组的值不会衰减为元素零的地址。让我们通过例子详细解释这些例外情况:

int a[2];

int *pi = a ; /* the same as pi = &a[0]; */

printf("%d\n", sizeof(a)); /* size of the array, not of a pointer is printed! */

int (*pi2)[2] = &a; /* address of the array itself is taken (not the address of a pointer) */

最后

char a[] = "Hello world ";

这里不是复制了指向“Hello world”的指针,而是复制了整个字符串,并且 a 指向该副本。

这里的信息非常详细,一次性理解所有内容确实很难,所以请慢慢来。我建议您先阅读 K&R 上关于这个主题的内容,然后再阅读 这本 绝佳的书籍。


12
+1,在C++调试时平静开发人员通常是第一步。 :D - Russell
这不是一个警告,而是两种类型的二进制布局中的根本不兼容性。 - Chris Lutz
2
注意,虽然 b 是有效的并且可以编译,但是为了使用它来访问 a 的元素,你需要指定边界,即 int (*b)[2] = a; - AusCBloke
我会说类似于“如果你将int[]赋值给int*,编译器会自动进行转换”(你在回答中的例子中进行了说明)。我不喜欢使用“decay”这个术语,因为访问int*需要比访问int[]更多的步骤。 - Timothy Jones
3
好的回答(太多人误解了这一点)。建议阅读:comp.lang.d FAQ 的第6节。 - Keith Thompson
显示剩余13条评论

7

这是一个经常出现的问题,我将尽力清晰地解释它。

当您创建一个数组时,它会将元素在内存中连续存储,因此:

int arr[2] = { 1, 2 };

Translates to:

arr:
+---+---+
| 1 | 2 |
+---+---+

指针指向内存中的一个对象,当通过一元运算符*[]对其进行解引用时,会访问该连续内存。因此,在...之后。
int *ptr = arr;

ptr(或者如果你喜欢,&ptr[0])指向盒子中的数字 1,而 ptr + 1(或者 &ptr[1])指向盒子中的数字 2。这很有道理。

但是,如果数组在内存中是连续的,那么数组的数组也是连续的。因此:

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

在内存中看起来像这样:

arr:
+---+---+---+---+
| 1 | 2 | 3 | 4 |
+---+---+---+---+

这看起来与我们的平面数组非常相似。

现在,让我们考虑一个指向指针的 int 在内存中的布局:

ptr:
+-------+-------+
| &sub1 | &sub2 |
+-------+-------+

sub1:
+---+---+
| 1 | 2 |
+---+---+

sub2:
+---+---+
| 3 | 4 |
+---+---+

ptr(或&ptr[0])指向sub1ptr + 1(或&ptr[1])指向sub2。尽管sub1sub2在内存中可能不相邻且没有实际的关系,但由于它是指针的指针,所以即使内存结构不兼容,2D数组的双重解引用也得以保留。

类型为T的数组会衰变成指向类型为T的指针,但是类型为T的数组的数组不会衰变成指向类型为T的指针的指针,而是衰变成指向类型为T [n]的数组的指针。因此,当我们的2D arr衰变为指针时,它不是一个指向int的指针,而是一个指向int [2]的指针。该类型的完整名称为int (*)[2],为了使您的代码行起作用,您需要使用:

int (*ptr)[2] = arr;

这是正确的类型。`ptr`期望指向一个连续的内存数组,就像`arr`一样 - `ptr`(或`&ptr[0]`)指向`arr`,`ptr + 1`(或`&ptr[1]`)指向`&arr[1]`。`ptr[0]`指向包含`1`的盒子,而`ptr[1]`指向包含`3`的盒子,因此`ptr [0] [0]`产生1,`ptr [0] [1]`产生2,以此类推。
为什么需要知道这个?二维指针似乎比它们值得复杂 - 如果您使用`malloc`,则必须在循环中重复调用`malloc`,并对`free`执行相同的操作。或者,您可以使用一些邪恶的*诡计,使平坦的,一维的内存分配行为像2D数组:
// x and y are the first and second dimensions of your array
// so it would be declared T arr[x][y] if x and y were static

int (*arr)[y] = malloc(x * y * sizeof(arr[0][0]));
if(!arr) /* error */;

现在arr指向一个大小为yint对象数组的连续块。由于它指向的是一个数组对象,我们不需要int **对象中的双指针间接引用,当您完成后,可以通过一次调用进行释放:
free(arr);

与使用int **版本相比,进行比较:
int **arr = malloc(x * sizeof(*arr));
if(!arr) /* error */;
for(size_t ii = 0; ii < x; ii++)
  {
    arr[ii] = malloc(y * sizeof(**arr));
    if(!arr[ii])
      {
        free(arr[ii]);
        free(arr);
      }
  }
// do work
for(size_t ii = 0; ii < x; ii++)
    free(arr[ii]);
free(arr);

上面的代码存在内存泄漏。看看你能否找到它。(或者只使用那些看似棘手的指向数组的指针版本。)


顺便提一下,int (*ptr)[2] 可能看起来很困惑,这是有道理的。命令行(现在也有网页版)工具 cdecl 可以帮助您将 C 声明解码为普通英语。 - Chris Lutz

4
著名的“衰变约定”:数组被视为指向数组第一个元素的指针。
int a[2] = {1,2};
int* b = a; //decay

但是,降解约定不应该对同一对象应用多次。
int a[2][2]={1,2,3,4};
int** b = a; //decay more than once

2

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接