C 中二维数组之我的理解

用类型系统理解二维数组并探索一下其的内存排布。

#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. 二维数组本质依旧是一维数组,只是指向的数据是另外的数组
    //    二维数组的第二层维度一旦被强制去除,将表现出与一维数组类似的行为

}

发表回复

您的电子邮箱地址不会被公开。 必填项已用 * 标注

此站点使用Akismet来减少垃圾评论。了解我们如何处理您的评论数据