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

C 语言指针和多维数组详解

2024-03-195.5k 阅读

C 语言指针和多维数组详解

在 C 语言编程中,指针和多维数组是非常重要且强大的概念。理解它们不仅对于编写高效的代码至关重要,也是深入掌握 C 语言特性的关键。本文将详细剖析 C 语言中指针与多维数组的关系,以及它们在实际编程中的应用。

指针基础回顾

在深入探讨指针与多维数组的关系之前,先简要回顾一下指针的基本概念。指针是一个变量,其值为另一个变量的内存地址。通过指针,我们可以间接访问和修改其他变量的值。

例如,下面是一个简单的指针示例:

#include <stdio.h>

int main() {
    int num = 10;
    int *ptr;

    ptr = &num; // 将 ptr 指向 num 的地址

    printf("num 的值: %d\n", num);
    printf("ptr 指向的地址: %p\n", (void*)ptr);
    printf("通过 ptr 访问 num 的值: %d\n", *ptr);

    return 0;
}

在上述代码中,int *ptr 声明了一个指向 int 类型变量的指针 ptrptr = &numptr 指向 num 的内存地址。通过 *ptr 可以间接访问 num 的值。

多维数组基础

多维数组是数组的数组。在 C 语言中,最常见的多维数组是二维数组,它可以看作是一个表格,有行和列。例如,声明一个二维数组 int arr[3][4],它表示有 3 行 4 列的整数数组。

二维数组的初始化方式有多种,以下是一些常见的方式:

// 方式一:逐元素初始化
int arr1[3][4] = {
    {1, 2, 3, 4},
    {5, 6, 7, 8},
    {9, 10, 11, 12}
};

// 方式二:按顺序初始化
int arr2[3][4] = {1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12};

// 方式三:部分初始化
int arr3[3][4] = {
    {1, 2},
    {5}
};

在内存中,二维数组是按行顺序存储的。也就是说,arr[0][0] 之后紧接着是 arr[0][1],然后是 arr[0][2],以此类推,直到 arr[0][3],接着是 arr[1][0] 等。

指针与二维数组的关系

  1. 二维数组名作为指针 在 C 语言中,二维数组名可以看作是一个指向数组首行的指针。例如,对于二维数组 int arr[3][4]arr 是一个指向 arr[0](即第一行数组)的指针。arr 的类型是 int (*)[4],表示指向包含 4 个 int 类型元素的数组的指针。

下面通过代码示例来理解:

#include <stdio.h>

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

    printf("arr 的值: %p\n", (void*)arr);
    printf("arr[0] 的值: %p\n", (void*)arr[0]);

    return 0;
}

在上述代码中,arrarr[0] 的值是相同的,都指向数组的首地址。但它们的类型不同,arr 是指向包含 4 个 int 类型元素的数组的指针,而 arr[0] 是指向 int 类型的指针(因为 arr[0] 本身是一个一维数组名,一维数组名是指向数组首元素的指针)。

  1. 指针运算与二维数组 由于 arr 是指向数组首行的指针,我们可以通过指针运算来访问二维数组的元素。例如,arr + 1 指向数组的第二行。因为 arr 的类型是 int (*)[4],所以 arr + 1 的地址偏移量是 4 * sizeof(int),即一行的大小。

同样,*(arr + 1) 等价于 arr[1],它是指向第二行首元素的指针。进一步,*(*(arr + 1) + 2) 等价于 arr[1][2],用于访问第二行第三列的元素。

下面是一个完整的代码示例:

#include <stdio.h>

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

    // 通过指针运算访问元素
    printf("arr[1][2] 的值: %d\n", *(*(arr + 1) + 2));

    return 0;
}
  1. 指向二维数组元素的指针 我们也可以声明一个指向二维数组单个元素的指针。例如:
int *ptr = &arr[0][0];

通过这个指针,我们可以像访问一维数组一样访问二维数组的元素。但需要注意的是,这种方式需要自己处理行和列的偏移关系。例如,要访问 arr[1][2],可以通过 ptr + 1 * 4 + 2 来实现(假设每行有 4 个元素)。

下面是一个示例:

#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];

    // 通过指向单个元素的指针访问元素
    printf("arr[1][2] 的值: %d\n", *(ptr + 1 * 4 + 2));

    return 0;
}

传递二维数组给函数

在 C 语言中,当我们将二维数组传递给函数时,有几种常见的方式。

  1. 使用数组形式 函数参数可以声明为二维数组的形式,例如:
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;
}
  1. 使用指针形式 由于二维数组名可以看作是指向数组首行的指针,函数参数也可以声明为指针形式:
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");
    }
}

这种方式与数组形式本质上是相同的,只是语法不同。在函数内部,两种方式都可以通过 arr[i][j]*(*(arr + i) + j) 来访问数组元素。

三维及多维数组

  1. 三维数组的声明与初始化 三维数组可以看作是数组的数组的数组。例如,声明一个三维数组 int arr[2][3][4],它表示有 2 个三维块,每个块有 3 行 4 列。

三维数组的初始化方式如下:

// 初始化三维数组
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}
    }
};

在内存中,三维数组也是按顺序存储的,先存储第一个三维块,再存储第二个三维块。

  1. 三维数组与指针 类似于二维数组,三维数组名可以看作是指向数组首三维块(即第一个二维数组)的指针。其类型为 int (*)[3][4]

例如,对于三维数组 arrarr 指向 arr[0]arr + 1 指向 arr[1]*(arr + 1) 等价于 arr[1],它是一个指向包含 4 个 int 类型元素的二维数组的指针。

通过指针运算访问三维数组元素的方式如下:

#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}
        }
    };

    // 通过指针运算访问元素
    printf("arr[1][2][3] 的值: %d\n", *(*(*(arr + 1) + 2) + 3));

    return 0;
}
  1. 传递三维数组给函数 当将三维数组传递给函数时,函数参数可以声明为三维数组形式或指针形式。例如:
// 数组形式
void print3DArray(int arr[][3][4], int slices) {
    for (int i = 0; i < slices; 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");
    }
}

// 指针形式
void print3DArray(int (*arr)[3][4], int slices) {
    for (int i = 0; i < slices; 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");
    }
}

在调用函数时,传递三维数组名即可:

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}
        }
    };

    print3DArray(arr, 2);

    return 0;
}
  1. 更高维度的数组 理论上,C 语言可以支持任意维度的数组,但随着维度的增加,代码的复杂度也会急剧上升。更高维度数组与指针的关系和操作方式与二维、三维数组类似,只是指针运算会更加复杂。例如,对于四维数组 int arr[2][3][4][5],数组名 arr 的类型为 int (*)[3][4][5],通过指针运算访问元素的方式会更加繁琐,如 *(*(*(*(arr + i) + j) + k) + l) 用于访问 arr[i][j][k][l]

指针数组与数组指针

  1. 指针数组 指针数组是一个数组,其元素都是指针。例如,int *arr[5] 声明了一个包含 5 个 int 类型指针的数组。

指针数组常用于处理多个字符串,因为字符串在 C 语言中是以字符数组的形式存储,而字符数组名可以看作是指向字符的指针。例如:

#include <stdio.h>

int main() {
    char *strs[3] = {
        "Hello",
        "World",
        "C Language"
    };

    for (int i = 0; i < 3; i++) {
        printf("%s\n", strs[i]);
    }

    return 0;
}

在上述代码中,strs 是一个指针数组,每个元素都是指向一个字符串的指针。

  1. 数组指针 数组指针是一个指针,它指向一个数组。例如,int (*ptr)[4] 声明了一个指向包含 4 个 int 类型元素的数组的指针。我们在前面讨论二维数组与指针关系时已经接触过数组指针,二维数组名本质上就是一个数组指针。

下面通过代码示例来区分指针数组和数组指针:

#include <stdio.h>

int main() {
    // 指针数组
    int *ptrArray[3];
    int num1 = 10, num2 = 20, num3 = 30;
    ptrArray[0] = &num1;
    ptrArray[1] = &num2;
    ptrArray[2] = &num3;

    // 数组指针
    int arr[4] = {1, 2, 3, 4};
    int (*arrPtr)[4] = &arr;

    printf("指针数组访问元素: %d\n", *ptrArray[1]);
    printf("数组指针访问元素: %d\n", (*arrPtr)[2]);

    return 0;
}

在上述代码中,ptrArray 是指针数组,arrPtr 是数组指针。

动态分配多维数组内存

  1. 使用 malloc 分配二维数组内存 在某些情况下,我们需要在运行时动态分配二维数组的内存。可以通过 malloc 函数来实现。例如,要分配一个 rowscols 列的二维数组:
#include <stdio.h>
#include <stdlib.h>

int main() {
    int rows = 3, cols = 4;
    int **arr = (int **)malloc(rows * sizeof(int *));
    for (int i = 0; i < rows; i++) {
        arr[i] = (int *)malloc(cols * sizeof(int));
    }

    // 初始化数组
    for (int i = 0; i < rows; i++) {
        for (int j = 0; j < cols; j++) {
            arr[i][j] = i * cols + j;
        }
    }

    // 打印数组
    for (int i = 0; i < rows; i++) {
        for (int j = 0; j < cols; j++) {
            printf("%d ", arr[i][j]);
        }
        printf("\n");
    }

    // 释放内存
    for (int i = 0; i < rows; i++) {
        free(arr[i]);
    }
    free(arr);

    return 0;
}

在上述代码中,首先通过 malloc 分配了一个包含 rowsint * 类型指针的数组,然后为每一行分配了包含 colsint 类型元素的内存。注意,在使用完后要释放分配的内存,以避免内存泄漏。

  1. 使用 calloc 分配二维数组内存 calloc 函数与 malloc 类似,但它会将分配的内存初始化为 0。例如:
#include <stdio.h>
#include <stdlib.h>

int main() {
    int rows = 3, cols = 4;
    int **arr = (int **)calloc(rows, sizeof(int *));
    for (int i = 0; i < rows; i++) {
        arr[i] = (int *)calloc(cols, sizeof(int));
    }

    // 打印数组(此时数组元素都为 0)
    for (int i = 0; i < rows; i++) {
        for (int j = 0; j < cols; j++) {
            printf("%d ", arr[i][j]);
        }
        printf("\n");
    }

    // 释放内存
    for (int i = 0; i < rows; i++) {
        free(arr[i]);
    }
    free(arr);

    return 0;
}
  1. 动态分配三维及更高维度数组内存 动态分配三维数组内存的方式类似,但更加复杂。例如,要分配一个 slices 个三维块,每个块有 rowscols 列的三维数组:
#include <stdio.h>
#include <stdlib.h>

int main() {
    int slices = 2, rows = 3, cols = 4;
    int ***arr = (int ***)malloc(slices * sizeof(int **));
    for (int i = 0; i < slices; i++) {
        arr[i] = (int **)malloc(rows * sizeof(int *));
        for (int j = 0; j < rows; j++) {
            arr[i][j] = (int *)malloc(cols * sizeof(int));
        }
    }

    // 初始化数组
    for (int i = 0; i < slices; i++) {
        for (int j = 0; j < rows; j++) {
            for (int k = 0; k < cols; k++) {
                arr[i][j][k] = i * rows * cols + j * cols + k;
            }
        }
    }

    // 打印数组
    for (int i = 0; i < slices; i++) {
        for (int j = 0; j < rows; j++) {
            for (int k = 0; k < cols; k++) {
                printf("%d ", arr[i][j][k]);
            }
            printf("\n");
        }
        printf("\n");
    }

    // 释放内存
    for (int i = 0; i < slices; i++) {
        for (int j = 0; j < rows; j++) {
            free(arr[i][j]);
        }
        free(arr[i]);
    }
    free(arr);

    return 0;
}

更高维度数组的动态内存分配以此类推,但随着维度增加,代码的复杂度和内存管理的难度也会显著增加。

通过深入理解 C 语言指针和多维数组的关系及操作方式,我们能够编写出更高效、灵活的代码,充分发挥 C 语言的强大功能。无论是在系统编程、嵌入式开发还是其他领域,这些知识都具有重要的应用价值。在实际编程中,应根据具体需求合理选择使用指针和多维数组,同时注意内存管理,避免出现内存泄漏等问题。