引言:二维数组的困惑
假设有这样一个C语言的2维数组:
int arr[3][4] = { {1,2,3,4}, {5,6,7,8}, {9,10,11,12} };
那么,数组名arr指向哪里?
它 *arr指向哪里?
它arr[0]又指向哪里?
而它&arr[0]呢?
它 *arr[0]呢?
那它&arr又指向哪里?
又如何将一个二维数组的地址赋值给一个二维数组的指针呢?
在C语言中,二维数组的使用一定是拦路虎般的存在,它令许多C,C++程序员望而却步。
带着上边这些疑问,今天这个视频我就和大家一起来弄清楚C语言的二维数组的用法。

二维数组的定义与初始化
int matrix[2][3];
这行代码定义了一个2行3列的整型二维数组。 matrix可以看作一个矩阵,[2]表示有2行,[3]表示每行有3个元素。
而对这个二维数组进行初始化,你可以这样写:
int matrix[2][3] = {1,2,3,4,5,6};
咦,你前边不是说它可以理解成一个2行3列的矩阵吗,为什么可以这样来初始化这个数组呢?
这是因为二维数组在内存中的存储形式是连续的。 内存中并没有“表格”的概念。
当然你也可以采用花括号内嵌套花括号,从而对应行与列的结构来初始化二维数组:
int matrix[2][3] = {{1, 2, 3}, {4, 5, 6}};
但虽然你采用这样的方法定义了一个2维数组,但其实它在内存中仍然是连续存放的。
二维数组在内存中的实际存储方式
我们可以运行这段代码,然后将变量拉入内存中查看。
可以看到内存中的值就是连续存放的。
有朋友可能会说,但是为什么在调试器中查看这个变量matrix时,为什么看到的又是2行3列的呢?
这只是为了方便程序员理解数据的逻辑结构,
二维数组本质上还是一个一维的内存块,只是在语法上提供了二维的访问方式。
逐个击破二维数组指针问题
既然如此,我们再回过头来看视频开头那个二维数组:
int arr[3][4] = {
{1,2,3,4},
{5,6,7,8},
{9,10,11,12}
};
现在让我们来逐个击破那些令人头疼的问题。
1. 数组名 arr 指向哪里?
在C语言中,数组名在大多数情况下会“退化”为指向其首元素的指针。
对于二维数组 arr[3][4] 来说,arr会退化为指向 int[4] 类型的指针,也就是 int (*)[4](一个指向包含4个 int 的数组的指针)。
可以把内存想象成这样:
arr –> +——————-+
| 1 2 3 4 | arr[0]
+——————-+
| 5 6 7 8 | arr[1]
+——————-+
| 9 10 11 12 | arr[2]
+——————-+
arr 指向的是: {1,2,3,4} 这一整行。
你可以把它理解成:arr == &arr[0]
所以此时 arr + 1 会跳过整整一行(4个int)。
也就是跳到:{5,6,7,8}
2. *arr 指向哪里?
因为前面的视频我们已经知道arr 指向第一行一维数组的指针,
所以对 arr 解引用,也就是会把“第一行”取出来“,就会得到 arr 所指向的内容——也就是第一个一维数组,
而这个一维数组的数组名同样会退化,变为指向其首元素(即第0行的第0列)的指针,类型是 int *。
所以这样一行代码
printf("**arr = %d\n", **arr);
输出第一个元素的值:1
是不是有点绕呢?
3. arr[0] 又指向哪里?
arr[0] 是二维数组的第0行,它本身是一个含有4个 int 的一维数组。在表达式中,它退化指向 arr[0][0],类型也是 int *。
因此,arr[0] 和 *arr 在值和类型上完全相同。
4. &arr[0] 呢?
基于前边的视频,我们知道 arr[0] 是一个数组(也就是这个二维数组的第0行)。再取它的地址 &arr[0] 得到的是指向整个第0行数组的指针,
类型为 int (*)[4]。这是不是就和 arr 退化前的类型、值完全一样了。
但是这里注意是的:
arr[0] 退化为 int * 指向 arr[0][0] (arr 的第0行第0列)。
&arr[0] 是 int (*)[4](指向 int[4] 数组的指针),指向整个第0行。
虽然它们指向的内存起始地址相同,但指针类型不同,意义完全不同,因此进行指针运算时的步长不同。
如果你执行: arr[0] + 1,会移动:sizeof(int) 也就是跳到元素 2
但是你执行:&arr[0] + 1,会移动: 4×sizeof(int) 直接跳过整整一行,到: arr[1]
5. *arr[0] 呢?
根据运算符优先级,[] 高于 *,所以 *arr[0] 等价于 *(arr[0])。
我们知道 arr[0] 会退化为 int * 指向第0行首元素。
再对它解引用得到该元素的值,也就是 1。
所以 *arr[0] 是一个整数值,不是指针。
6. &arr 又指向哪里?
arr 是整个二维数组的名字。 &arr对整个数组再取地址,得到指向整个二维数组的指针,类型为 int (*)[3][4](指向“3行4列的二维数组”的指针)。
显然,&arr 的值也是二维数组的起始地址,但指针类型完全不同:
arr(退化后)是:int (*)[4] (一个指向包含4个 int数 的数组的指针)
而 &arr 会指向:int (*)[3][4](指向“3行4列的二维数组”的指针)
这4个地址(arr、&arr、&arr[0],arr[0])在数值上虽然相同,但它们意义完全不同,所指向的对象大小不同:
arr 步长:4个 int长度(16字节,假设int占4字节)
&arr[0] 步长:同样4个 int长度
&arr 步长:整个数组 3×4=12 个 int(假设int占4字节,那就是48字节)
如何正确将二维数组地址赋值给指针
最后,要如何将二维数组的地址赋值给一个指向二维数组的指针呢?
你需要声明一个指向整个二维数组的指针,其类型必须与 &arr 匹配:
也就是 int (*p)[3][4] (指向“3行4列的二维数组”的指针) = &arr;
如果你想通过这个指针访问元素,可以像这样:
printf("%d\n", (*p)[1][2]);
它会输出 7
不过更常见的访问二维数组的做法是首先将它指向“一维数组(也就是每行)”的指针,即 int (*p)[4](一个指向包含4个 int数 的数组的指针) = arr;,
然后用 p[i][j] 访问。
核心总结
最后你只要记住这句话,先看类型,再谈地址。
地址的数值一样,但指针类型不同,指针运算的步长就完全不同。
怎么样,你现在绕晕了吗?


