今天在科协和罗阳豪大佬关于二维数组和指针的用法上产生了一点分歧。

在谈到二维数组和指针的时候,我们都认同二维数组不应该等同于指针。然而在二维数组是不是一个指向分配地址空间第一位的二维指针的问题上,罗大佬却提出了这样的一种观点:就是二维数组是指向一个一维指针数组的指针,然后一维指针数组的各个元素再指向数组的每一列。

然而我却觉得,作为一个栈空间存在的东西,数组名应该就是一个指向存储体本身的指针。

问题的现象

然后罗大佬写了一个类似这样的程序:

1
2
3
4
5
6
7
8
9
10
11
#include <stdio.h>

void func1(int **p){
printf("%d", p[3][2]);
}

int main() {
int a[5][4];
a[3][2] = 5;
func1(a);
}

这个程序在编译之后的输出结果:在Windows下没有输出结果,在Mac上确实输出了一个值,但当然不会是5。

这个时候让我们在主函数调用func1之前和func1里面加上一点东西,让我们能看到各自相应位置的地址:

1
2
printf("%p\n", a);
printf("%p\n", &(a[3][2]))

输出如下:

$ ./a.out
0x7fff5dfd1690
0x7fff5dfd16c8
0x7fff5dfd1690
0x1092cb528
0

我们可以看到后输出的第两个值明显已经与前面的第二个值不相等了。这说明在将双重指针的p当作一个数组的过程中出现了一点意外。

然后根据输出在前面的位置可知,正确的让输出5的语法应该是

1
2
3
printf("%p\n", a);
printf("%p\n", (*a + 7));
printf("%d\n", *(*a+7));

但是我没有明白为什么是7,7好像跟3、2没有什么关联。当时我觉得,可能是先存了五个的指针,然后再存的当前使用的那一行,好像恰恰能解释得通。

问题的探究

这种解释我觉得很难接受,因为我无法想象为什么会耗费五个多余的空间存下五个指针。于是我再去问了姐姐。

姐姐当即表示,你这样看地址没有用,你需要将矩阵的所有元素都初始化,然后再看。这时就能看出一点问题了。

于是,将原本代码中的a[3][2] = 5这一行改成了下面这样:

1
2
3
4
5
6
int i, j;
for (i = 0; i < 5 ; i++) {
for (j = 0; j < 4; j++) {
a[i][j] = i*4 + j;
}
}

输出结果如下:

$ ./a.out
0x7fff5b2a3690
0x7fff5b2a36c8
0x7fff5b2a3690
0x70000000e
[1]    78696 segmentation fault  ./a.out

好哇这回结果是直接来了一个segmentation fault。但是只观察上面的代码就能看到一点端倪。

那就是最后一个地址:0x70000000e最后的e就是十进制的14,也就是3*4+2。

这说明的是这里的其实不应该是在往下寻址,而是直接输出就是我们所想要的那个元素了。

然后这时我忽然明白了为什么是+7,因为地址的长度是int的两倍,我需要加的是7*8个bit的距离来到达14*4的也就是a[3][2]的位置。

问题的解析

这个问题的解释是这样的:(当然这段是来自我无所不能的姐姐)

二维指针 **a 实际是 *(*a+x)+y
二维数组 a[M][N] 实际是*(a+x*M+y)

这个解释的证据是如果我们写这样的两个函数

1
2
3
4
5
6
7
8
// temp.c
int func1(int **a) {
return a[3][2];
}

int func2(int (*a)[4]) {
return a[3][2];
}

然后用编译器编译,使它们产生汇编代码 clang temp.c -S -o- 当然姐姐使用的命令带了一长串的参数来去掉多余的输出:

clang -fno-asynchronous-unwind-tables -fno-exceptions -fno-rtti -fverbose-asm -Wall -Wextra  temp.c -O3 -masm=intel -S -o-    

如果记不住这么一长串的参数,姐姐还推荐使用线上的编译器,结果会更直观。

然后你就能看到它们的代码是类似这样的:

func1(int**):
  mov rax, QWORD PTR [rdi+24]
  mov eax, DWORD PTR [rax+8]
  ret
func2(int (*) [4]):
  mov eax, DWORD PTR [rdi+56]
  ret

前者是先取了 $ a+324 $ 的位置然后再取的+8的位置然后返回。而后者是直接取了 $ 344 + 2*4 = 56 $ 的位置。

于是此事可以到此有一个结束了:二维数组在栈中是连续的存储单元,数组名是指向其首部的二维指针。而二维指针的取用方式和二维数组的取用方式有明显的区别,因此这两个东西不能混用。

问题的结论:

  1. 尽量少用2D array
  2. 避免使用多维指针,除非有足够原因
  3. 使用C的时候,尽量用自己定义的数据类型。

上面三条都是姐姐给我的忠告。下面这条是我的最后结论:

姐姐真是聪明而且温柔。

记得之前我也是看过这些东西的呢,看来是学得太慢,忘得太快,还得加倍努力才行啊。