用类型系统理解二维数组并探索一下其的内存排布。
#include <stdio.h>
//void f(double []); // double[] 等价于 double*,但若是多维数组,只能省略第一维
//void f(double **); // double[][4] 不与 double** 等价,因为第二维度不可省略
void f(double [][4]); // double(*)[4] 与 double[][4] 等价
int main() {
double a[3][4];
for (int i=0; i<3; ++i) {
for (int j=0; j<4; ++j) {
a[i][j] = i+j/10.0;
printf("[%d, %d] => %.1lfn", i, j, a[i][j]);
}
}
// Output:
// [0, 0] => 0.0
// [0, 1] => 0.1
// [0, 2] => 0.2
// [0, 3] => 0.3
// [1, 0] => 1.0
// [1, 1] => 1.1
// [1, 2] => 1.2
// [1, 3] => 1.3
// [2, 0] => 2.0
// [2, 1] => 2.1
// [2, 2] => 2.2
// [2, 3] => 2.3
f(a);
}
void f(double (*a)[4]) {
// a 是一个指针,指向的数据类型为“double [4]”
printf("%d %d %dn%d %d %dn",
sizeof(a), sizeof(a[0]), sizeof(a[0][0]),
// 指针(一般)为 int,所以 4
// 将 a 视作数组,取得第一个元素(类型为 double [4]),
// 所以 sizeof(double)*4 = 32
// a[0] 取得类型为 double [4] 的元素,对这个元素再次取下标,得到 double
sizeof(double), sizeof(double*), sizeof(double**));
// Output:
// 8 32 8
// 8 8 8
// 阅读下文时,时刻记得,在内存中,这段数组不会额外储存任何指针
// 真实情况是只有 3*4 个 double 型的数据紧密排列
for (int i=0; i<12; ++i)
printf("[%4d] => %.1lfn", i, ( *a ) [i] );
// *a 得到 double [4],取下标可得 double,
// 此例说明二维数组数据也是紧凑地并排放置
// Output:
// [ 0] => 0.0
// [ 1] => 0.1
// [ 2] => 0.2
// [ 3] => 0.3
// [ 4] => 1.0
// [ 5] => 1.1
// [ 6] => 1.2
// [ 7] => 1.3
// [ 8] => 2.0
// [ 9] => 2.1
// [ 10] => 2.2
// [ 11] => 2.3
for (int i=0; i<8; ++i)
printf("[%4d] => %.1lfn", i, ( *(a + 1) ) [i] );
// a+1 (类型依旧是指向 double[4] 的指针)指向将 a 视为数组的下一个元素,
// 即跳过了第一个 double[4]
// 然后解引用得到第二个 double[4],再取下标,得到第二行开始的各个 double
// Output:
// [ 0] => 1.0
// [ 1] => 1.1
// [ 2] => 1.2
// [ 3] => 1.3
// [ 4] => 2.0
// [ 5] => 2.1
// [ 6] => 2.2
// [ 7] => 2.3
for (int i=0; i<8; ++i)
printf("[%4d] => %.1lfn", i, ( (double *) a + 1 ) [i] );
// 先让 a 的类型从 double(*)[4] 变为 double(*)
// 通过强制转换,我们故意让类型系统丢失了 a 的二维维度这个信息
// 现在可以看成:a 是一个指针,但指向的数据类型不再是“double [4]”
// 而是“double”(丢了维度信息)
// 现在,double* 型的 a+1 不再是指向下一个 double[4],
// 而是指向往后跳过一个 double 的内存处
// 接着,将 a+1 这个指针视作数组,取下标得到 double
// 可以发现,这一次跳过的不是一行,而是一个元素(一个 double)
// Output:
// [ 0] => 0.1
// [ 1] => 0.2
// [ 2] => 0.3
// [ 3] => 1.0
// [ 4] => 1.1
// [ 5] => 1.2
// [ 6] => 1.3
// [ 7] => 2.0
// 综上,通过实验可得到以下结论:
// 1. 二维数组在内存里依旧是线性储存,前行尾与后行首是完全紧挨着的
// 2. 在传递参数的过程中,因为编译器会自动解开第一层数组,转换成指针
// 本来就已经丢失第一维的长度了,所以函数原型可忽略第一维度
// 但不可忽略后续维度
// 3. 对于编译器,指针指向的类型十分重要
// 知道了指针指向什么数据才可以对其进行 +1、解引用等操作
// 4. 二维数组本质依旧是一维数组,只是指向的数据是另外的数组
// 二维数组的第二层维度一旦被强制去除,将表现出与一维数组类似的行为
}