MK
摩柯社区 - 一个极简的技术知识社区
AI 面试

C语言多维数组的存储顺序与访问方式

2022-12-082.9k 阅读

C语言多维数组的存储顺序

二维数组的存储顺序

在C语言中,二维数组在内存中是以行优先(Row - major order)的方式存储的。这意味着数组元素按行依次存储在连续的内存位置上。

假设有一个二维数组int arr[3][4];,可以将其想象成一个3行4列的表格。在内存中,它的存储顺序是先存储第一行的4个元素,接着存储第二行的4个元素,最后存储第三行的4个元素。

从内存地址的角度来看,数组的起始地址就是第一个元素arr[0][0]的地址。假设每个int类型占用4个字节(在32位系统下常见),那么arr[0][1]的地址就是arr[0][0]的地址加上4(因为arr[0][1]arr[0][0]之后的一个int类型元素),arr[1][0]的地址则是arr[0][0]的地址加上4 * 4(因为第一行有4个int类型元素,要跳过第一行才能到第二行的第一个元素)。

以下是通过代码来验证二维数组行优先存储顺序的示例:

#include <stdio.h>

int main() {
    int arr[3][4] = {
        {1, 2, 3, 4},
        {5, 6, 7, 8},
        {9, 10, 11, 12}
    };
    int *ptr = &arr[0][0];
    for (int i = 0; i < 3 * 4; i++) {
        printf("%d ", *(ptr + i));
    }
    printf("\n");
    return 0;
}

在上述代码中,我们定义了一个二维数组arr,并通过一个int类型的指针ptr指向数组的首元素arr[0][0]。然后通过指针遍历数组,按照内存中的存储顺序输出所有元素。运行该程序,输出结果将是:1 2 3 4 5 6 7 8 9 10 11 12,这清晰地展示了行优先的存储顺序。

三维数组的存储顺序

对于三维数组,同样遵循行优先的存储规则,但更加复杂。以int arr[2][3][4];为例,可以把它看作是由2个“页”,每个“页”是一个3行4列的二维数组。

在内存中,首先存储第一“页”的所有元素,按照行优先的顺序存储完第一“页”的3行4列元素后,接着存储第二“页”的元素,同样按照行优先的顺序。

假设每个int类型占用4个字节,arr[0][0][0]是数组的起始地址。那么arr[0][0][1]的地址是arr[0][0][0]的地址加上4,arr[0][1][0]的地址是arr[0][0][0]的地址加上4 * 4(因为要跳过第一行的4个元素),arr[1][0][0]的地址是arr[0][0][0]的地址加上3 * 4 * 4(因为要跳过第一“页”,第一“页”有3行4列,共3 * 4个元素)。

以下代码展示三维数组的存储顺序验证:

#include <stdio.h>

int main() {
    int arr[2][3][4] = {
        {
            {1, 2, 3, 4},
            {5, 6, 7, 8},
            {9, 10, 11, 12}
        },
        {
            {13, 14, 15, 16},
            {17, 18, 19, 20},
            {21, 22, 23, 24}
        }
    };
    int *ptr = &arr[0][0][0];
    for (int i = 0; i < 2 * 3 * 4; i++) {
        printf("%d ", *(ptr + i));
    }
    printf("\n");
    return 0;
}

运行上述代码,输出结果为:1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24,验证了三维数组也是按行优先顺序存储。

更高维度数组的存储顺序

从二维和三维数组的存储顺序可以推广到更高维度的数组。无论数组是几维的,C语言都遵循行优先的存储规则。对于一个n维数组int arr[d1][d2]...[dn];,在内存中先存储第一组d2 * d3 *... * dn个元素(按照行优先顺序),然后依次存储后续组的元素。

例如,对于四维数组int arr[2][3][4][5];,先存储第一组3 * 4 * 5个元素(这一组元素内部按行优先存储),再存储第二组3 * 4 * 5个元素。这种存储方式使得在访问数组元素时,可以通过一定的计算来准确找到所需元素在内存中的位置。

C语言多维数组的访问方式

二维数组的访问方式

  1. 标准下标访问方式 最常见的访问二维数组元素的方式是使用下标。对于二维数组int arr[3][4];,可以通过arr[i][j]来访问第i行第j列的元素,其中i的取值范围是0到2(因为有3行),j的取值范围是0到3(因为有4列)。例如,arr[1][2]表示访问第二行第三列的元素。

    以下是一个简单的示例,用于初始化并访问二维数组:

#include <stdio.h>

int main() {
    int arr[3][4];
    for (int i = 0; i < 3; i++) {
        for (int j = 0; j < 4; j++) {
            arr[i][j] = i * 4 + j + 1;
        }
    }
    for (int i = 0; i < 3; i++) {
        for (int j = 0; j < 4; j++) {
            printf("%d ", arr[i][j]);
        }
        printf("\n");
    }
    return 0;
}

在上述代码中,外层循环控制行,内层循环控制列。通过arr[i][j]的方式,我们可以方便地初始化和访问二维数组的每个元素。

  1. 指针偏移访问方式 由于二维数组在内存中是连续存储的,也可以使用指针偏移的方式来访问元素。我们知道二维数组名可以看作是指向数组首行的指针,而首行又可以看作是一个一维数组。所以arr实际上是一个指向int[4]类型的指针(假设arrint arr[3][4];)。

    要访问arr[i][j],可以通过以下方式计算地址:*(arr + i * 4 + j)。这里arr是数组首地址,i * 4表示跳过i行(因为每行有4个元素),再加上j就是要访问元素的偏移量。

    以下是使用指针偏移访问二维数组的示例:

#include <stdio.h>

int main() {
    int arr[3][4];
    int *ptr = &arr[0][0];
    for (int i = 0; i < 3; i++) {
        for (int j = 0; j < 4; j++) {
            *(ptr + i * 4 + j) = i * 4 + j + 1;
        }
    }
    for (int i = 0; i < 3; i++) {
        for (int j = 0; j < 4; j++) {
            printf("%d ", *(ptr + i * 4 + j));
        }
        printf("\n");
    }
    return 0;
}

三维数组的访问方式

  1. 标准下标访问方式 对于三维数组int arr[2][3][4];,使用下标访问元素的方式为arr[i][j][k],其中i表示“页”的索引,取值范围是0到1;j表示行的索引,取值范围是0到2;k表示列的索引,取值范围是0到3。

    例如,arr[1][2][3]表示访问第二“页”第三行第四列的元素。

    以下代码展示三维数组的初始化和通过下标访问:

#include <stdio.h>

int main() {
    int arr[2][3][4];
    int num = 1;
    for (int i = 0; i < 2; i++) {
        for (int j = 0; j < 3; j++) {
            for (int k = 0; k < 4; k++) {
                arr[i][j][k] = num++;
            }
        }
    }
    for (int i = 0; i < 2; i++) {
        for (int j = 0; j < 3; j++) {
            for (int k = 0; k < 4; k++) {
                printf("%d ", arr[i][j][k]);
            }
            printf("\n");
        }
        printf("\n");
    }
    return 0;
}
  1. 指针偏移访问方式 三维数组的指针偏移访问稍微复杂一些。对于int arr[2][3][4];,数组名arr是一个指向int[3][4]类型的指针。

    要访问arr[i][j][k],其地址计算方式为*(arr + i * 3 * 4 + j * 4 + k)。这里i * 3 * 4表示跳过i个“页”(每个“页”有3 * 4个元素),j * 4表示在当前“页”中跳过j行(每行有4个元素),再加上k就是要访问元素的偏移量。

    以下是使用指针偏移访问三维数组的示例:

#include <stdio.h>

int main() {
    int arr[2][3][4];
    int *ptr = &arr[0][0][0];
    int num = 1;
    for (int i = 0; i < 2; i++) {
        for (int j = 0; j < 3; j++) {
            for (int k = 0; k < 4; k++) {
                *(ptr + i * 3 * 4 + j * 4 + k) = num++;
            }
        }
    }
    for (int i = 0; i < 2; i++) {
        for (int j = 0; j < 3; j++) {
            for (int k = 0; k < 4; k++) {
                printf("%d ", *(ptr + i * 3 * 4 + j * 4 + k));
            }
            printf("\n");
        }
        printf("\n");
    }
    return 0;
}

更高维度数组的访问方式

  1. 标准下标访问方式 对于n维数组int arr[d1][d2]...[dn];,通过arr[i1][i2]...[in]的方式来访问元素,其中i1的取值范围是0到d1 - 1i2的取值范围是0到d2 - 1,以此类推,in的取值范围是0到dn - 1

    例如,对于五维数组int arr[2][3][4][5][6];,访问元素arr[1][2][3][4][5]就是获取特定位置的元素。

  2. 指针偏移访问方式 对于n维数组,指针偏移访问元素的地址计算公式为:*(arr + i1 * d2 * d3 *... * dn + i2 * d3 * d4 *... * dn +... + in - 1 * dn + in)

    其中,arr是数组首地址,i1in是各维度的索引,d1dn是各维度的大小。这种计算方式基于数组的行优先存储顺序,通过各维度的索引和维度大小来准确计算元素在内存中的偏移量。

多维数组存储顺序与访问方式的应用场景

矩阵运算

在矩阵运算中,二维数组经常被用来表示矩阵。由于二维数组按行优先存储,对于矩阵的按行遍历操作非常高效。例如矩阵乘法运算,假设我们有两个矩阵AB,结果矩阵为C,其计算方式为C[i][j] = ∑(A[i][k] * B[k][j]),这里ijk是循环变量。在实现矩阵乘法时,利用二维数组的行优先存储顺序,可以使得内存访问更加连续,提高缓存命中率,从而提升运算效率。

以下是一个简单的矩阵乘法示例:

#include <stdio.h>

void matrixMultiply(int A[][100], int B[][100], int C[][100], int m, int n, int p) {
    for (int i = 0; i < m; i++) {
        for (int j = 0; j < p; j++) {
            C[i][j] = 0;
            for (int k = 0; k < n; k++) {
                C[i][j] += A[i][k] * B[k][j];
            }
        }
    }
}

int main() {
    int A[100][100], B[100][100], C[100][100];
    int m, n, p;
    printf("Enter the number of rows of matrix A: ");
    scanf("%d", &m);
    printf("Enter the number of columns of matrix A and rows of matrix B: ");
    scanf("%d", &n);
    printf("Enter the number of columns of matrix B: ");
    scanf("%d", &p);

    printf("Enter elements of matrix A:\n");
    for (int i = 0; i < m; i++) {
        for (int j = 0; j < n; j++) {
            scanf("%d", &A[i][j]);
        }
    }

    printf("Enter elements of matrix B:\n");
    for (int i = 0; i < n; i++) {
        for (int j = 0; j < p; j++) {
            scanf("%d", &B[i][j]);
        }
    }

    matrixMultiply(A, B, C, m, n, p);

    printf("Resultant matrix C:\n");
    for (int i = 0; i < m; i++) {
        for (int j = 0; j < p; j++) {
            printf("%d ", C[i][j]);
        }
        printf("\n");
    }

    return 0;
}

图像处理

在图像处理中,图像通常可以表示为三维数组,例如对于RGB图像,可以用int image[height][width][3];来表示,其中height是图像的高度,width是图像的宽度,第三维的3分别表示红(R)、绿(G)、蓝(B)三个颜色通道。由于三维数组按行优先存储,在对图像进行逐行处理,如边缘检测、滤波等操作时,可以高效地访问每个像素点的颜色通道值。

例如,简单的灰度化处理(将RGB图像转换为灰度图像)可以通过以下代码实现:

#include <stdio.h>

void grayscale(int image[][100][3], int height, int width) {
    for (int i = 0; i < height; i++) {
        for (int j = 0; j < width; j++) {
            int gray = (image[i][j][0] + image[i][j][1] + image[i][j][2]) / 3;
            image[i][j][0] = gray;
            image[i][j][1] = gray;
            image[i][j][2] = gray;
        }
    }
}

int main() {
    int image[100][100][3];
    int height, width;
    printf("Enter the height of the image: ");
    scanf("%d", &height);
    printf("Enter the width of the image: ");
    scanf("%d", &width);

    printf("Enter RGB values for each pixel:\n");
    for (int i = 0; i < height; i++) {
        for (int j = 0; j < width; j++) {
            for (int k = 0; k < 3; k++) {
                scanf("%d", &image[i][j][k]);
            }
        }
    }

    grayscale(image, height, width);

    printf("Grayscale image:\n");
    for (int i = 0; i < height; i++) {
        for (int j = 0; j < width; j++) {
            printf("%d ", image[i][j][0]);
        }
        printf("\n");
    }

    return 0;
}

立体数据表示

在一些科学计算和工程领域,如地理信息系统(GIS)中表示三维地形数据,或者在医学图像处理中表示三维体数据等场景下,会用到三维或更高维度的数组。例如,三维地形数据可以用float terrain[depth][height][width];来表示,其中depth表示地形的垂直深度,heightwidth分别表示地形在水平方向上的高度和宽度。通过合理利用多维数组的存储顺序和访问方式,可以高效地对这些数据进行分析和处理,如计算地形的坡度、提取特定区域的数据等。

以下是一个简单的示例,用于计算三维地形数据中某一点的坡度(假设简单的一阶差分计算坡度):

#include <stdio.h>
#include <math.h>

float calculateSlope(float terrain[][100][100], int depth, int height, int width, int i, int j, int k) {
    float dx = 0, dy = 0, dz = 0;
    if (i > 0) dx = terrain[i - 1][j][k] - terrain[i][j][k];
    if (j > 0) dy = terrain[i][j - 1][k] - terrain[i][j][k];
    if (k > 0) dz = terrain[i][j][k - 1] - terrain[i][j][k];
    return sqrt(dx * dx + dy * dy + dz * dz);
}

int main() {
    float terrain[100][100][100];
    int depth, height, width;
    printf("Enter the depth of the terrain data: ");
    scanf("%d", &depth);
    printf("Enter the height of the terrain data: ");
    scanf("%d", &height);
    printf("Enter the width of the terrain data: ");
    scanf("%d", &width);

    printf("Enter terrain values:\n");
    for (int i = 0; i < depth; i++) {
        for (int j = 0; j < height; j++) {
            for (int k = 0; k < width; k++) {
                scanf("%f", &terrain[i][j][k]);
            }
        }
    }

    int x, y, z;
    printf("Enter the coordinates (x, y, z) to calculate slope: ");
    scanf("%d %d %d", &x, &y, &z);

    float slope = calculateSlope(terrain, depth, height, width, x, y, z);
    printf("Slope at (%d, %d, %d) is: %f\n", x, y, z, slope);

    return 0;
}

多维数组访问时的注意事项

边界检查

在访问多维数组时,一定要进行边界检查。对于二维数组int arr[3][4];,如果访问arr[3][0]或者arr[0][4]等超出边界的位置,会导致未定义行为。这种未定义行为可能会使程序崩溃,或者产生难以调试的错误结果。

在编写代码时,要确保所有的索引值都在合法范围内。例如,在使用循环遍历二维数组时:

#include <stdio.h>

int main() {
    int arr[3][4] = {
        {1, 2, 3, 4},
        {5, 6, 7, 8},
        {9, 10, 11, 12}
    };
    for (int i = 0; i < 3; i++) {
        for (int j = 0; j < 4; j++) {
            printf("%d ", arr[i][j]);
        }
        printf("\n");
    }
    return 0;
}

上述代码中,i的循环条件是i < 3j的循环条件是j < 4,保证了不会访问到数组边界之外的元素。

指针类型与数组类型的匹配

当使用指针偏移方式访问多维数组时,要确保指针类型与数组元素类型匹配。例如,对于二维数组int arr[3][4];,如果使用int *ptr = &arr[0][0];来指向数组首元素,在计算偏移量并访问元素时,要按照int类型的大小来计算偏移。

如果错误地将指针类型定义为char *,例如char *ptr = (char *)&arr[0][0];,在计算偏移量*(ptr + i * 4 + j)时,由于char类型通常是1个字节,而int类型通常是4个字节,会导致访问到错误的内存位置,从而产生未定义行为。

数组作为函数参数

当多维数组作为函数参数传递时,需要注意数组退化的问题。在C语言中,数组作为函数参数传递时,会退化为指针。例如,对于二维数组int arr[3][4];,函数声明不能写成void func(int arr[3][4]);,而应该写成void func(int arr[][4], int rows);,其中rows表示数组的行数。因为在函数内部,编译器无法通过数组名获取到数组的第一维大小,需要额外传递。

以下是一个示例:

#include <stdio.h>

void printArray(int arr[][4], int rows) {
    for (int i = 0; i < rows; i++) {
        for (int j = 0; j < 4; j++) {
            printf("%d ", arr[i][j]);
        }
        printf("\n");
    }
}

int main() {
    int arr[3][4] = {
        {1, 2, 3, 4},
        {5, 6, 7, 8},
        {9, 10, 11, 12}
    };
    printArray(arr, 3);
    return 0;
}

在上述代码中,printArray函数的参数arr退化为指向int[4]类型的指针,通过额外传递的rows参数来确定数组的行数,从而正确地访问和输出二维数组的元素。对于更高维度的数组作为函数参数传递时,同样需要遵循类似的规则,除了第一维大小外,其他维度大小需要在函数声明中明确指定。