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

C语言一维数组下标引用的技巧

2023-07-067.2k 阅读

C 语言一维数组下标引用的基本概念

在 C 语言中,数组是一种非常重要的数据结构,它允许我们在内存中连续存储多个相同类型的元素。一维数组是最简单的数组形式,它只有一个维度。而下标引用则是访问数组元素的关键操作。

数组下标基础

数组下标从 0 开始计数。假设有一个整型数组 int arr[5];,这个数组包含 5 个元素,它们的下标分别是 0 到 4。我们可以通过以下方式访问数组元素:

#include <stdio.h>

int main() {
    int arr[5] = {10, 20, 30, 40, 50};
    printf("arr[0] 的值是: %d\n", arr[0]);
    printf("arr[3] 的值是: %d\n", arr[3]);
    return 0;
}

在上述代码中,arr[0] 访问的是数组的第一个元素,值为 10;arr[3] 访问的是数组的第四个元素,值为 40。

下标引用与内存地址关系

数组在内存中是连续存储的。以 int 类型为例,假设 int 类型占用 4 个字节的内存空间。对于数组 int arr[5];,如果 arr 的起始地址为 &arr[0],那么 &arr[1] 的地址就是 &arr[0] + 4(因为一个 int 占 4 个字节),&arr[2] 的地址是 &arr[0] + 8,依此类推。

从编译器的角度看,当下标引用数组元素 arr[i] 时,它实际上计算的地址是 &arr[0] + i * sizeof(int)。这种基于内存地址的计算方式是理解下标引用本质的关键。

利用下标引用实现数组操作

遍历数组

遍历数组是最常见的操作之一,通过下标引用可以轻松实现。

#include <stdio.h>

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

在上述代码中,for 循环使用下标 i 从 0 到 4 遍历数组,依次输出每个元素的值。

数组元素的修改

通过下标引用不仅可以读取数组元素的值,还可以修改它们。

#include <stdio.h>

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

这段代码首先遍历数组,将每个元素的值乘以 2,然后再次遍历输出修改后的值。

下标引用的边界问题

越界访问

在使用下标引用时,一个常见的错误是越界访问。例如:

#include <stdio.h>

int main() {
    int arr[5] = {1, 2, 3, 4, 5};
    // 尝试访问越界元素
    printf("arr[5] = %d\n", arr[5]);
    return 0;
}

在上述代码中,数组 arr 的有效下标范围是 0 到 4,但这里尝试访问 arr[5],这是越界访问。这种行为在 C 语言中是未定义的,可能会导致程序崩溃、数据损坏或其他不可预测的结果。

防止越界的方法

为了防止越界访问,在使用下标引用时,一定要确保下标在有效范围内。一种常见的方法是在循环遍历数组时,正确设置循环条件。

#include <stdio.h>

int main() {
    int arr[5] = {1, 2, 3, 4, 5};
    // 正确的遍历,防止越界
    for (int i = 0; i < 5; i++) {
        printf("arr[%d] = %d\n", i, arr[i]);
    }
    return 0;
}

这里 for 循环的条件 i < 5 确保了 i 的值始终在有效下标范围内。

灵活运用下标引用技巧

逆序访问数组

通过灵活使用下标,可以实现数组的逆序访问。

#include <stdio.h>

int main() {
    int arr[5] = {1, 2, 3, 4, 5};
    for (int i = 4; i >= 0; i--) {
        printf("arr[%d] = %d\n", i, arr[i]);
    }
    return 0;
}

在上述代码中,for 循环从数组的最后一个元素的下标 4 开始,递减到 0,实现了逆序访问数组。

跳跃访问数组

有时我们可能需要跳跃式地访问数组元素。例如,每隔一个元素访问一次。

#include <stdio.h>

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

这里 for 循环的步长设置为 2,使得每次循环 i 增加 2,从而实现每隔一个元素访问一次数组。

利用下标进行数组查找

可以利用下标引用来实现简单的数组查找操作。例如,查找数组中是否存在某个特定的值。

#include <stdio.h>

int main() {
    int arr[5] = {10, 20, 30, 40, 50};
    int target = 30;
    int found = 0;
    for (int i = 0; i < 5; i++) {
        if (arr[i] == target) {
            printf("找到了 %d,下标是 %d\n", target, i);
            found = 1;
            break;
        }
    }
    if (!found) {
        printf("%d 未在数组中找到\n", target);
    }
    return 0;
}

在这段代码中,通过遍历数组并使用下标引用,检查每个元素是否等于目标值 target。如果找到,则输出其下标并结束循环;如果遍历完整个数组都未找到,则输出未找到的提示。

与指针结合的下标引用技巧

指针与数组的关系

在 C 语言中,指针和数组有着密切的关系。数组名在大多数情况下可以看作是一个指向数组首元素的指针常量。例如,对于数组 int arr[5];arr 等同于 &arr[0]

利用指针进行下标引用

我们可以通过指针来实现类似数组下标的访问。

#include <stdio.h>

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

在上述代码中,定义了一个指针 ptr 指向数组 arr 的首元素。*(ptr + i) 这种形式类似于数组的下标引用 arr[i],它通过指针偏移来访问数组元素。

指针运算与下标引用的等价性

从本质上讲,arr[i]*(arr + i) 是等价的。编译器在处理这两种形式时,都会将其转换为基于内存地址的计算。例如,对于 arr[i],编译器会计算 &arr[0] + i * sizeof(int),而对于 *(arr + i) 同样是计算 &arr[0] + i * sizeof(int) 并取该地址处的值。

多维数组中的下标引用技巧(以二维数组为例延伸讲解)

虽然我们主要讨论一维数组,但了解多维数组中的下标引用技巧有助于更深入理解下标引用的本质。

二维数组的内存存储

二维数组在内存中也是按顺序连续存储的。例如,对于二维数组 int arr[3][4];,它在内存中的存储顺序是先存储第一行的元素,再存储第二行,以此类推。即 arr[0][0], arr[0][1], arr[0][2], arr[0][3], arr[1][0], arr[1][1], ...

二维数组的下标引用

我们可以通过两个下标来访问二维数组中的元素。

#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("arr[%d][%d] = %d ", i, j, arr[i][j]);
        }
        printf("\n");
    }
    return 0;
}

在上述代码中,外层循环控制行下标 i,内层循环控制列下标 j,通过双重循环遍历二维数组并输出每个元素的值。

从一维数组角度理解二维数组下标引用

实际上,二维数组可以看作是一种特殊的一维数组。例如,int arr[3][4]; 可以看作是一个包含 3 个元素的一维数组,每个元素又是一个包含 4 个 int 类型元素的一维数组。从内存地址计算的角度看,arr[i][j] 的地址计算为 &arr[0][0] + i * 4 * sizeof(int) + j * sizeof(int),这里 i * 4 是因为每一行有 4 个元素,先定位到第 i 行的起始地址,再加上 j * sizeof(int) 定位到该行的第 j 个元素。这种理解方式有助于我们将一维数组下标引用的技巧延伸到二维数组甚至更高维数组。

下标引用在函数中的应用

传递数组到函数

在 C 语言中,当我们将数组作为参数传递给函数时,实际上传递的是数组首元素的地址。例如:

#include <stdio.h>

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

int main() {
    int arr[5] = {1, 2, 3, 4, 5};
    printArray(arr, 5);
    return 0;
}

在上述代码中,printArray 函数接受一个整型数组和数组的大小作为参数。虽然函数定义中数组参数写成 int arr[] 的形式,但实际上它等同于 int *arr,传递的是数组首元素的地址。在函数内部,可以通过下标引用 arr[i] 来访问数组元素。

在函数中修改数组元素

由于传递的是数组首元素的地址,所以在函数中对数组元素的修改会影响到原数组。

#include <stdio.h>

void doubleArray(int arr[], int size) {
    for (int i = 0; i < size; i++) {
        arr[i] = arr[i] * 2;
    }
}

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

doubleArray 函数中,通过下标引用修改了数组元素的值,回到 main 函数中,原数组的值也已经被修改。

二维数组作为函数参数

当二维数组作为函数参数时,情况稍微复杂一些。函数参数必须指定除第一维以外的其他维的大小。例如:

#include <stdio.h>

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

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

print2DArray 函数中,参数 int arr[][4] 表示这是一个二维数组,第二维的大小必须明确指定为 4。这样编译器才能正确计算数组元素的地址,在函数内部才能通过 arr[i][j] 正确访问二维数组的元素。

结合结构体与数组下标引用

结构体数组

结构体数组是一种包含多个结构体元素的数组。例如,定义一个表示学生信息的结构体,并创建结构体数组。

#include <stdio.h>
#include <string.h>

struct Student {
    char name[20];
    int age;
    float score;
};

int main() {
    struct Student students[3];
    strcpy(students[0].name, "Alice");
    students[0].age = 20;
    students[0].score = 85.5;

    strcpy(students[1].name, "Bob");
    students[1].age = 21;
    students[1].score = 90.0;

    strcpy(students[2].name, "Charlie");
    students[2].age = 22;
    students[2].score = 88.0;

    for (int i = 0; i < 3; i++) {
        printf("学生 %d: 姓名 %s, 年龄 %d, 分数 %.2f\n", i + 1, students[i].name, students[i].age, students[i].score);
    }
    return 0;
}

在上述代码中,通过数组下标 students[i] 访问结构体数组中的每个结构体元素,然后再通过结构体成员访问符 . 访问结构体的具体成员。

结构体中包含数组

结构体中也可以包含数组成员。例如,定义一个表示矩阵的结构体,其中矩阵元素存储在一个二维数组中。

#include <stdio.h>

struct Matrix {
    int data[3][3];
};

void printMatrix(struct Matrix mat) {
    for (int i = 0; i < 3; i++) {
        for (int j = 0; j < 3; j++) {
            printf("%d ", mat.data[i][j]);
        }
        printf("\n");
    }
}

int main() {
    struct Matrix mat = {
        {1, 2, 3},
        {4, 5, 6},
        {7, 8, 9}
    };
    printMatrix(mat);
    return 0;
}

这里通过 mat.data[i][j] 访问结构体 Matrix 中二维数组的数据成员,实现矩阵的输出。

下标引用与动态内存分配

动态分配一维数组

在 C 语言中,可以使用 malloc 函数动态分配内存来创建一维数组。

#include <stdio.h>
#include <stdlib.h>

int main() {
    int n = 5;
    int *arr = (int *)malloc(n * sizeof(int));
    if (arr == NULL) {
        printf("内存分配失败\n");
        return 1;
    }
    for (int i = 0; i < n; i++) {
        arr[i] = i * 10;
    }
    for (int i = 0; i < n; i++) {
        printf("arr[%d] = %d\n", i, arr[i]);
    }
    free(arr);
    return 0;
}

在上述代码中,使用 malloc 函数分配了一块能容纳 nint 类型元素的内存空间,并将返回的指针赋值给 arr。之后可以像使用普通数组一样通过下标引用 arr[i] 来访问和修改动态分配数组的元素。最后,使用 free 函数释放分配的内存。

动态分配二维数组

动态分配二维数组稍微复杂一些。一种常见的方法是先分配一个指针数组,每个指针再指向一个动态分配的一维数组。

#include <stdio.h>
#include <stdlib.h>

int main() {
    int rows = 3;
    int cols = 4;
    int **arr = (int **)malloc(rows * sizeof(int *));
    if (arr == NULL) {
        printf("内存分配失败\n");
        return 1;
    }
    for (int i = 0; i < rows; i++) {
        arr[i] = (int *)malloc(cols * sizeof(int));
        if (arr[i] == NULL) {
            printf("内存分配失败\n");
            for (int j = 0; j < i; j++) {
                free(arr[j]);
            }
            free(arr);
            return 1;
        }
    }
    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("arr[%d][%d] = %d ", i, j, arr[i][j]);
        }
        printf("\n");
    }
    for (int i = 0; i < rows; i++) {
        free(arr[i]);
    }
    free(arr);
    return 0;
}

在这段代码中,首先分配一个包含 rowsint * 类型的指针数组 arr。然后,为每个指针分配一个能容纳 colsint 类型元素的一维数组。这样就创建了一个动态的二维数组,可以通过 arr[i][j] 来访问和修改元素。最后,需要先释放每个一维数组的内存,再释放指针数组的内存。

优化下标引用的性能考虑

缓存友好性

现代计算机的 CPU 都配备了缓存,缓存的存在可以大大提高内存访问的速度。在使用数组下标引用时,尽量保证访问的连续性,以提高缓存命中率。例如,顺序遍历数组比跳跃式访问数组更有利于缓存利用。

#include <stdio.h>
#include <time.h>

#define SIZE 1000000

int main() {
    int arr[SIZE];
    for (int i = 0; i < SIZE; i++) {
        arr[i] = i;
    }
    clock_t start = clock();
    // 顺序访问
    for (int i = 0; i < SIZE; i++) {
        int temp = arr[i];
    }
    clock_t end = clock();
    double time_seq = (double)(end - start) / CLOCKS_PER_SEC;

    start = clock();
    // 跳跃访问
    for (int i = 0; i < SIZE; i += 10) {
        int temp = arr[i];
    }
    end = clock();
    double time_jump = (double)(end - start) / CLOCKS_PER_SEC;

    printf("顺序访问时间: %f 秒\n", time_seq);
    printf("跳跃访问时间: %f 秒\n", time_jump);
    return 0;
}

在上述代码中,分别测试了顺序访问和跳跃访问数组的时间。一般情况下,顺序访问由于缓存友好性,会比跳跃访问更快。

减少不必要的计算

在使用下标引用时,尽量减少下标表达式中的复杂计算。例如:

#include <stdio.h>

int main() {
    int arr[10];
    int base = 5;
    // 避免复杂计算
    for (int i = 0; i < 10; i++) {
        arr[i] = i * base;
    }
    // 不推荐的复杂计算
    // for (int i = 0; i < 10; i++) {
    //     arr[(i * 2 + 3) % 10] = i;
    // }
    return 0;
}

在第一个 for 循环中,下标 i 是简单的变量,而在第二个注释掉的 for 循环中,下标 (i * 2 + 3) % 10 包含了复杂的计算。复杂的下标计算可能会增加 CPU 的负担,降低程序性能,应尽量避免。

通过深入理解 C 语言一维数组下标引用的各种技巧,包括基本概念、常见操作、边界问题、与指针的结合、在函数中的应用、与结构体的结合、动态内存分配以及性能优化等方面,我们能够更加高效地使用数组,编写出健壮且性能优良的 C 语言程序。在实际编程中,根据具体的需求和场景,灵活运用这些技巧,能够解决各种与数组相关的问题,提升程序的质量和效率。同时,不断实践和总结经验,对于进一步掌握 C 语言数组以及整个语言体系都有着重要的意义。