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

C语言编程二维数组深入讲解

2021-07-134.0k 阅读

二维数组的基础概念

在 C 语言中,二维数组可以看作是数组的数组。它可以用来表示具有行和列结构的数据,比如矩阵、棋盘等。

二维数组的声明

二维数组的声明语法如下:

数据类型 数组名[行数][列数];

例如,声明一个整型的二维数组 a,有 3 行 4 列:

int a[3][4];

这里,a 就是二维数组名,[3] 表示有 3 行,[4] 表示每行有 4 列。数组元素的类型为 int

二维数组的内存布局

在内存中,二维数组是按行顺序存储的。以 int a[3][4] 为例,它的存储顺序是先存储第一行的 4 个元素,接着存储第二行的 4 个元素,最后存储第三行的 4 个元素。

假设 a 的起始地址为 0x1000,每个 int 类型占 4 个字节,那么 a[0][0] 的地址就是 0x1000a[0][1] 的地址是 0x1004a[1][0] 的地址是 0x1010(因为第一行有 4 个 int,占 16 个字节)。

二维数组的初始化

  1. 完全初始化 可以像下面这样对二维数组进行完全初始化:
int a[3][4] = {
    {1, 2, 3, 4},
    {5, 6, 7, 8},
    {9, 10, 11, 12}
};

这里,大括号内的每一个小括号表示一行的数据。第一行是 {1, 2, 3, 4},第二行是 {5, 6, 7, 8},第三行是 {9, 10, 11, 12}

  1. 部分初始化 也可以进行部分初始化,未初始化的元素会被自动初始化为 0。例如:
int a[3][4] = {
    {1, 2},
    {5}
};

此时,a[0][0] 为 1,a[0][1] 为 2,a[1][0] 为 5,其他元素均为 0。

  1. 省略行数初始化 当初始化数据时,可以省略行数,但列数不能省略。例如:
int a[][4] = {
    {1, 2, 3, 4},
    {5, 6, 7, 8}
};

编译器会根据初始化的数据行数自动确定数组的行数为 2。

二维数组的访问

通过下标访问

二维数组的元素可以通过双下标来访问,语法为 数组名[行下标][列下标]。行下标和列下标都是从 0 开始的。

例如,对于前面声明并初始化的 int a[3][4]

#include <stdio.h>

int main() {
    int a[3][4] = {
        {1, 2, 3, 4},
        {5, 6, 7, 8},
        {9, 10, 11, 12}
    };
    printf("a[1][2]的值为: %d\n", a[1][2]);
    return 0;
}

在这个例子中,a[1][2] 访问的是第二行第三列的元素,输出结果为 7。

越界访问的问题

当访问二维数组时,如果行下标或列下标超出了数组声明的范围,就会发生越界访问。这可能会导致未定义行为,比如程序崩溃、数据损坏等。

例如,对于 int a[3][4],如果访问 a[3][0]a[0][4] 都是越界访问。

#include <stdio.h>

int main() {
    int a[3][4] = {
        {1, 2, 3, 4},
        {5, 6, 7, 8},
        {9, 10, 11, 12}
    };
    // 下面这行代码会导致越界访问,行为未定义
    printf("a[3][0]的值为: %d\n", a[3][0]);
    return 0;
}

在实际编程中,一定要确保下标在合法的范围内。

二维数组与指针

二维数组名与指针

二维数组名本身是一个指向数组首元素的指针。对于 int a[3][4]a 是一个指向 a[0] 的指针,而 a[0] 又是一个包含 4 个 int 类型元素的数组。所以,a 的类型是 int (*)[4],即指向包含 4 个 int 类型元素的数组的指针。

a + 1 会跳过一行(4 个 int 类型元素的空间,假设 int 占 4 个字节,就跳过 16 个字节),因为它是按行存储的。

指针访问二维数组元素

  1. 通过指针偏移访问 可以通过指针偏移来访问二维数组的元素。例如:
#include <stdio.h>

int main() {
    int a[3][4] = {
        {1, 2, 3, 4},
        {5, 6, 7, 8},
        {9, 10, 11, 12}
    };
    int *p = &a[0][0];
    printf("通过指针访问a[1][2]的值为: %d\n", *(p + 1 * 4 + 2));
    return 0;
}

这里,p 指向 a[0][0],要访问 a[1][2],因为是按行存储,所以偏移量为 1 * 4 + 2(行号乘以列数加上列号)。

  1. 使用指向数组的指针访问 还可以使用指向数组的指针来访问二维数组元素。例如:
#include <stdio.h>

int main() {
    int a[3][4] = {
        {1, 2, 3, 4},
        {5, 6, 7, 8},
        {9, 10, 11, 12}
    };
    int (*p)[4] = a;
    printf("通过指向数组的指针访问a[1][2]的值为: %d\n", (*(p + 1))[2]);
    return 0;
}

这里,p 是一个指向包含 4 个 int 类型元素数组的指针,p + 1 指向第二行,*(p + 1) 就是第二行数组,再通过 [2] 访问第二行第三列的元素。

二维数组作为函数参数

二维数组作为参数的声明方式

  1. 完整声明二维数组参数 函数声明中可以完整地声明二维数组参数。例如:
void printArray(int a[3][4]) {
    for (int i = 0; i < 3; i++) {
        for (int j = 0; j < 4; j++) {
            printf("%d ", a[i][j]);
        }
        printf("\n");
    }
}
  1. 省略行数声明 也可以省略行数声明,但列数不能省略。例如:
void printArray(int a[][4]) {
    for (int i = 0; i < 3; i++) {
        for (int j = 0; j < 4; j++) {
            printf("%d ", a[i][j]);
        }
        printf("\n");
    }
}
  1. 使用指向数组的指针声明 还可以使用指向数组的指针来声明参数。例如:
void printArray(int (*a)[4]) {
    for (int i = 0; i < 3; i++) {
        for (int j = 0; j < 4; j++) {
            printf("%d ", (*(a + i))[j]);
        }
        printf("\n");
    }
}

传递二维数组到函数

在主函数中,可以将二维数组传递给上述函数。例如:

#include <stdio.h>

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

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

在这个例子中,main 函数将二维数组 a 传递给 printArray 函数,printArray 函数负责打印数组的内容。

二维数组的常见应用

矩阵运算

  1. 矩阵加法 假设有两个矩阵 AB,要计算它们的和 C。矩阵加法要求两个矩阵的行数和列数相同。
#include <stdio.h>

void matrixAdd(int A[][100], int B[][100], int C[][100], int rows, int cols) {
    for (int i = 0; i < rows; i++) {
        for (int j = 0; j < cols; j++) {
            C[i][j] = A[i][j] + B[i][j];
        }
    }
}

void printMatrix(int matrix[][100], int rows, int cols) {
    for (int i = 0; i < rows; i++) {
        for (int j = 0; j < cols; j++) {
            printf("%d ", matrix[i][j]);
        }
        printf("\n");
    }
}

int main() {
    int A[3][3] = {
        {1, 2, 3},
        {4, 5, 6},
        {7, 8, 9}
    };
    int B[3][3] = {
        {9, 8, 7},
        {6, 5, 4},
        {3, 2, 1}
    };
    int C[3][3];
    matrixAdd(A, B, C, 3, 3);
    printf("矩阵 A:\n");
    printMatrix(A, 3, 3);
    printf("矩阵 B:\n");
    printMatrix(B, 3, 3);
    printf("矩阵 A + B:\n");
    printMatrix(C, 3, 3);
    return 0;
}

在这个代码中,matrixAdd 函数实现了矩阵加法,printMatrix 函数用于打印矩阵。

  1. 矩阵乘法 矩阵乘法要求第一个矩阵的列数等于第二个矩阵的行数。假设矩阵 Am x n 的,矩阵 Bn x p 的,那么它们的乘积矩阵 Cm x p 的。
#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];
            }
        }
    }
}

void printMatrix(int matrix[][100], int rows, int cols) {
    for (int i = 0; i < rows; i++) {
        for (int j = 0; j < cols; j++) {
            printf("%d ", matrix[i][j]);
        }
        printf("\n");
    }
}

int main() {
    int A[2][3] = {
        {1, 2, 3},
        {4, 5, 6}
    };
    int B[3][2] = {
        {7, 8},
        {9, 10},
        {11, 12}
    };
    int C[2][2];
    matrixMultiply(A, B, C, 2, 3, 2);
    printf("矩阵 A:\n");
    printMatrix(A, 2, 3);
    printf("矩阵 B:\n");
    printMatrix(B, 3, 2);
    printf("矩阵 A * B:\n");
    printMatrix(C, 2, 2);
    return 0;
}

在这个代码中,matrixMultiply 函数实现了矩阵乘法,printMatrix 函数用于打印矩阵。

图像处理中的应用

在图像处理中,图像可以用二维数组来表示。例如,对于灰度图像,每个像素点的灰度值可以存储在二维数组的元素中。

假设我们要对一幅图像进行简单的亮度调整,将每个像素的灰度值增加一个固定值。

#include <stdio.h>

#define ROWS 100
#define COLS 100

void adjustBrightness(int image[ROWS][COLS], int value) {
    for (int i = 0; i < ROWS; i++) {
        for (int j = 0; j < COLS; j++) {
            if (image[i][j] + value <= 255) {
                image[i][j] += value;
            } else {
                image[i][j] = 255;
            }
        }
    }
}

void printImage(int image[ROWS][COLS]) {
    for (int i = 0; i < ROWS; i++) {
        for (int j = 0; j < COLS; j++) {
            printf("%3d ", image[i][j]);
        }
        printf("\n");
    }
}

int main() {
    int image[ROWS][COLS];
    // 初始化图像数据,这里简单地用 0 - 100 填充
    for (int i = 0; i < ROWS; i++) {
        for (int j = 0; j < COLS; j++) {
            image[i][j] = i * j % 100;
        }
    }
    printf("原始图像:\n");
    printImage(image);
    adjustBrightness(image, 50);
    printf("调整亮度后的图像:\n");
    printImage(image);
    return 0;
}

在这个例子中,adjustBrightness 函数实现了对图像亮度的调整,printImage 函数用于打印图像数据。实际的图像处理会涉及更复杂的算法和数据格式,但基本的二维数组操作是相似的。

游戏开发中的应用

在一些简单的游戏开发中,比如棋盘游戏,二维数组可以用来表示游戏棋盘的状态。

以井字棋游戏为例,棋盘可以用一个 3 x 3 的二维数组来表示,数组元素可以是 'X''O'' '(表示空格)。

#include <stdio.h>

void printBoard(char board[3][3]) {
    for (int i = 0; i < 3; i++) {
        for (int j = 0; j < 3; j++) {
            printf("%c ", board[i][j]);
        }
        printf("\n");
    }
}

int checkWin(char board[3][3], char player) {
    // 检查行
    for (int i = 0; i < 3; i++) {
        if (board[i][0] == player && board[i][1] == player && board[i][2] == player) {
            return 1;
        }
    }
    // 检查列
    for (int j = 0; j < 3; j++) {
        if (board[0][j] == player && board[1][j] == player && board[2][j] == player) {
            return 1;
        }
    }
    // 检查对角线
    if (board[0][0] == player && board[1][1] == player && board[2][2] == player) {
        return 1;
    }
    if (board[0][2] == player && board[1][1] == player && board[2][0] == player) {
        return 1;
    }
    return 0;
}

int main() {
    char board[3][3] = {
        {' ', ' ', ' '},
        {' ', ' ', ' '},
        {' ', ' ', ' '}
    };
    char currentPlayer = 'X';
    int gameOver = 0;
    while (!gameOver) {
        printBoard(board);
        int row, col;
        printf("玩家 %c 请输入行和列(0 - 2): ", currentPlayer);
        scanf("%d %d", &row, &col);
        if (row >= 0 && row < 3 && col >= 0 && col < 3 && board[row][col] == ' ') {
            board[row][col] = currentPlayer;
            if (checkWin(board, currentPlayer)) {
                printBoard(board);
                printf("玩家 %c 获胜!\n", currentPlayer);
                gameOver = 1;
            } else {
                currentPlayer = (currentPlayer == 'X')? 'O' : 'X';
            }
        } else {
            printf("无效的输入,请重新输入。\n");
        }
    }
    return 0;
}

在这个代码中,printBoard 函数用于打印棋盘,checkWin 函数用于检查玩家是否获胜。通过二维数组来管理棋盘状态,实现了井字棋游戏的基本逻辑。

二维数组与多维数组的关系

从二维数组到多维数组的扩展

二维数组可以看作是最简单的多维数组。多维数组就是数组的数组的数组……以此类推。例如,三维数组可以看作是由二维数组组成的数组。

声明一个三维数组的语法如下:

数据类型 数组名[第一维大小][第二维大小][第三维大小];

例如,声明一个整型的三维数组 a

int a[2][3][4];

这里,a 是一个三维数组,第一维有 2 个元素,每个元素又是一个二维数组,这个二维数组有 3 行 4 列。

多维数组的内存布局与访问

多维数组在内存中也是按顺序存储的。以 int a[2][3][4] 为例,它先存储第一维的第一个二维数组,再存储第一维的第二个二维数组。而每个二维数组又是按行顺序存储的。

访问多维数组元素同样通过多个下标,例如 a[1][2][3] 访问的是第一维的第二个二维数组中,第二行第三列的元素。

多维数组在实际中的应用

多维数组在一些复杂的数据结构和算法中有应用。比如在气象数据的存储中,如果要记录不同地点(可以看作第一维)、不同时间(第二维)、不同气象参数(第三维,如温度、湿度、气压等)的数据,就可以使用三维数组。

#include <stdio.h>

#define LOCATIONS 5
#define TIMES 10
#define PARAMETERS 3

void printWeatherData(int weather[LOCATIONS][TIMES][PARAMETERS]) {
    for (int location = 0; location < LOCATIONS; location++) {
        printf("地点 %d 的气象数据:\n", location);
        for (int time = 0; time < TIMES; time++) {
            printf("时间 %d: ", time);
            for (int parameter = 0; parameter < PARAMETERS; parameter++) {
                printf("%d ", weather[location][time][parameter]);
            }
            printf("\n");
        }
    }
}

int main() {
    int weather[LOCATIONS][TIMES][PARAMETERS];
    // 简单初始化数据
    for (int location = 0; location < LOCATIONS; location++) {
        for (int time = 0; time < TIMES; time++) {
            for (int parameter = 0; parameter < PARAMETERS; parameter++) {
                weather[location][time][parameter] = location * time * parameter;
            }
        }
    }
    printWeatherData(weather);
    return 0;
}

在这个例子中,printWeatherData 函数用于打印气象数据,通过三维数组 weather 来存储不同地点、不同时间、不同气象参数的数据。

总之,二维数组是多维数组的基础,理解二维数组的概念、操作和应用,对于掌握多维数组以及在实际编程中处理复杂数据结构都有重要的意义。同时,在使用二维数组和多维数组时,要注意内存管理、下标越界等问题,确保程序的正确性和稳定性。