08、数组介绍

厨子大约 12 分钟C语言基础程序程序厨

在C语言中,数组是一种非常重要的数据结构,它允许我们存储和管理固定大小的相同类型元素的集合。

本文将基于数组的基本概念,深入探讨其用法、特性以及一些高级技巧。

基本概念

数组是由具有相同数据类型的元素按连续内存位置存储的数据结构。在C语言中,数组的大小在定义时必须指定,且在整个生命周期内保持不变。数组的元素可以通过索引来访问,索引从0开始。

1. 数组的声明与初始化

数组的声明需要指定数据类型和数组的大小。例如,声明一个包含5个整数的数组:

int numbers[5];

此时,数组中的元素未被初始化,它们的值是未定义的。为了初始化数组,我们可以在声明时指定元素的值:

int numbers[5] = {1, 2, 3, 4, 5};

如果初始化时提供的元素数量少于数组的大小,剩余的元素将被自动初始化为0(对于基本数据类型):

int numbers[5] = {1, 2}; // numbers数组为 {1, 2, 0, 0, 0}

如果省略数组大小,编译器将根据初始化列表中的元素数量来自动确定数组的大小:

int numbers[] = {1, 2, 3, 4, 5}; // 编译器自动确定数组大小为5

2. 访问数组元素

数组元素可以通过数组名和索引来访问。索引从0开始,因此numbers[0]访问的是数组的第一个元素。

#include <stdio.h>

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

3. 数组的遍历

遍历数组是访问数组元素的常见操作,通常使用循环来实现。上面的示例已经展示了如何使用for循环来遍历数组,这里就不再次展示了。

数组的进阶用法

1. 多维数组

多维数组是数组的数组。最常见的是二维数组,它可以看作是一个表格,其中每个元素都是一个一维数组。

声明一个二维数组需要指定行数和列数:

int matrix[3][4]; // 3行4列的二维数组

初始化二维数组:

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

访问二维数组的元素需要使用两个索引,第一个索引表示行,第二个索引表示列:

#include <stdio.h>

int main() {
    int matrix[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("matrix[%d][%d] = %d\n", i, j, matrix[i][j]);
        }
    }
    return 0;
}

2. 数组作为函数参数

在C语言中,数组名代表数组的首地址,因此可以将数组作为函数参数传递。但是,需要注意的是,数组在作为函数参数时,会被退化为指向数组首元素的指针。

#include <stdio.h>

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

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

对于二维数组,需要传递数组的行数和列数,或者传递指向数组的指针(在这种情况下,通常需要使用额外的参数来指定数组的维度)。

#include <stdio.h>

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

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

3. 数组与指针

数组名和指针之间有着密切的关系。数组名可以被解释为指向数组首元素的指针。因此,可以使用指针来遍历和操作数组。

#include <stdio.h>

int main() {
    int numbers[5] = {1, 2, 3, 4, 5};
    int *ptr = numbers; // 指针指向数组首元素

    for (int i = 0; i < 5; i++) {
        printf("%d ", *(ptr + i)); // 使用指针遍历数组
    }
    printf("\n");

    return 0;
}

指针算术允许我们通过指针来访问数组的元素。ptr + i计算的是第i个元素的地址,*(ptr + i)则访问该地址处的值。

数组的扩展用法与技巧

1. 动态数组

数组的大小在定义时必须指定,且在整个生命周期内保持不变。然而,有时我们需要根据程序运行时的条件来动态分配数组的大小。这时,可以使用C标准库中的动态内存分配函数,如malloccallocrealloc

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

int main() {
    int n;
    printf("Enter the number of elements: ");
    scanf("%d", &n);

    int *arr = (int *)malloc(n * sizeof(int)); // 动态分配内存
    if (arr == NULL) {
        printf("Memory allocation failed\n");
        return 1;
    }

    // 初始化数组元素
    for (int i = 0; i < n; i++) {
        arr[i] = i + 1;
    }

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

    // 释放动态分配的内存
    free(arr);

    return 0;
}

在这个例子中,我们使用malloc函数动态分配了一个整数数组,并在使用完毕后使用free函数释放了内存。

2. 字符串与字符数组

字符串通常是通过字符数组来实现的。字符串以空字符\0结尾,因此字符数组的大小通常比字符串的实际长度多1。

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

int main() {
    char str[] = "Hello, World!"; // 自动包含空字符'\0'
    printf("String: %s\n", str);
    printf("Length of string: %lu\n", strlen(str)); // 使用strlen函数获取字符串长度
    return 0;
}

在这个例子中,str是一个字符数组,它存储了字符串"Hello, World!"以及一个空字符\0strlen函数用于获取字符串的长度,不包括空字符。

3. 数组与函数指针

函数指针是指向函数的指针。我们可以将函数指针作为数组的元素,从而创建一个函数指针数组。我们可以在运行时动态选择并调用函数。

#include <stdio.h>

void func1() {
    printf("Function 1\n");
}

void func2() {
    printf("Function 2\n");
}

int main() {
    void (*funcArray[2])() = {func1, func2}; // 函数指针数组

    for (int i = 0; i < 2; i++) {
        funcArray[i](); // 调用函数指针数组中的函数
    }

    return 0;
}

在这个例子中,我们定义了一个函数指针数组 funcArray,其中包含了两个函数的指针:func1func2。通过遍历这个数组,我们可以动态地调用这些函数。

数组的高级应用与注意事项

1. 数组与内存布局

在C语言中,数组在内存中是连续存储的。这意味着数组的元素在内存中的地址是连续的,这有助于快速访问和处理数组数据。

例如,对于一个整型数组 int arr[5];,其在内存中的布局可能如下(假设每个整数占用4个字节):

+---+---+---+---+---+
| 0 | 1 | 2 | 3 | 4 |  (索引)
+---+---+---+---+---+
|.. |.. |.. |.. |.. |  (内存地址,每个单元格4字节)
+---+---+---+---+---+

数组名 arr 实际上是指向数组第一个元素的指针,即 &arr[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[5] 是越界访问,因为数组 arr 的有效索引范围是 0 到 4。访问 arr[5] 将导致未定义行为。

为了避免数组越界,应该始终在访问数组元素时检查索引是否在有效范围内。

3. 数组与指针运算

虽然数组名可以被当作指针使用,但有一些细微的差别。数组名是一个常量指针,不能对其赋值或进行算术运算以改变其指向。然而,可以通过指针变量来遍历数组。

#include <stdio.h>

int main() {
    int arr[5] = {1, 2, 3, 4, 5};
    int *ptr = arr; // 指针指向数组首元素

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

    // 指针运算
    ptr += 2; // 指针移动到数组的第三个元素
    printf("After moving pointer, *ptr = %d\n", *ptr); // 输出3

    return 0;
}

在这个例子中,指针 ptr 被初始化为指向数组 arr 的首元素。通过指针运算 ptr += 2;,我们可以将指针移动到数组的第三个元素。

4. 多维数组的内存布局

多维数组在内存中也是连续存储的。对于二维数组 int matrix[3][4];,其在内存中的布局是行优先的(即先存储第一行的所有元素,再存储第二行的所有元素,依此类推)。

+---+---+---+---+---+---+---+---+---+---+---+---+
| 0,0| 0,1| 0,2| 0,3| 1,0| 1,1| 1,2| 1,3| 2,0| 2,1| ...
+---+---+---+---+---+---+---+---+---+---+---+---+

5. 数组与字符串处理

字符串是以空字符 \0 结尾的字符数组。处理字符串时,需要特别注意字符串的长度和空字符的存在。

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

int main() {
    char str[20] = "Hello, World!";
    printf("String: %s\n", str);
    printf("Length: %lu\n", strlen(str)); // 不包括空字符

    // 字符串拼接
    strcat(str, "!!!"); // 注意目标数组必须有足够的空间
    printf("After concatenation: %s\n", str);

    return 0;
}

在这个例子中,我们使用 strcat 函数将字符串 "!!!" 拼接到 str 的末尾。需要注意的是,目标数组 str 必须有足够的空间来存储拼接后的字符串,否则会导致数组越界和未定义行为。

回顾

基本声明与初始化

一维数组

// 声明一个整型数组,大小为5,未初始化(这里只是为了掌握概念,我们平时写代码可不建议声明后未初始化)
int arr[5];

// 声明并初始化一个整型数组
int arr[5] = {1, 2, 3, 4, 5};

// 部分初始化,未初始化的元素将自动初始化为0
int arr[5] = {1, 2}; // arr = {1, 2, 0, 0, 0}

多维数组

// 声明一个2x3的整型二维数组,未初始化
int arr[2][3];

// 声明并初始化一个2x3的整型二维数组
int arr[2][3] = {
    {1, 2, 3},
    {4, 5, 6}
};

// 部分初始化,未初始化的元素将自动初始化为0
int arr[2][3] = {
    {1, 2}
}; // arr = {{1, 2, 0}, {0, 0, 0}}

遍历

一维数组

int arr[5] = {1, 2, 3, 4, 5};

// 使用for循环遍历
for (int i = 0; i < 5; ++i) {
    std::cout << arr[i] << " ";
}

// 使用while循环遍历
int i = 0;
while (i < 5) {
    std::cout << arr[i] << " ";
    ++i;
}

多维数组

int arr[2][3] = {
    {1, 2, 3},
    {4, 5, 6}
};

// 使用嵌套的for循环遍历二维数组
for (int i = 0; i < 2; ++i) {
    for (int j = 0; j < 3; ++j) {
        std::cout << arr[i][j] << " ";
    }
    std::cout << std::endl;
}

元素访问与修改

一维数组

int arr[5] = {1, 2, 3, 4, 5};

// 访问数组元素
int value = arr[2]; // value = 3

// 修改数组元素
arr[2] = 10; // arr = {1, 2, 10, 4, 5}

// 思考并测试下,如果越界访问数据会发生什么?

多维数组

int arr[2][3] = {
    {1, 2, 3},
    {4, 5, 6}
};

// 访问二维数组元素
int value = arr[1][2]; // value = 6

// 修改二维数组元素
arr[1][2] = 9; // arr = {{1, 2, 3}, {4, 5, 9}}

数组操作

计算数组长度

在C++中,静态数组的长度是编译时确定的,可以使用sizeof运算符来计算数组的长度:

int arr[5];
int length = sizeof(arr) / sizeof(arr[0]); // length = 5

// 注意,一定要除以sizeof(arr[0])才能得到数组真正的长度,思考下为什么?

注意,对于动态分配的数组(如使用new关键字分配的数组),需要手动保存其长度,因为sizeof运算符无法直接应用于指针来获取动态数组的长度。

不支持动态改变大小和直接在数组中插入/删除元素

C++标准数组不支持动态改变大小或直接在数组中插入/删除元素。如果需要这些功能,可以使用std::vector等容器。

排序

简单举个冒泡排序的示例,方便我们深入理解数组:

可以直接点此链接 https://godbolt.org/z/1orWW1beMopen in new window 查看代码。

#include <iostream>

void bubbleSort(int arr[], int n) {
    for (int i = 0; i < n-1; ++i) {
        for (int j = 0; j < n-i-1; ++j) {
            if (arr[j] > arr[j+1]) {
                // 交换元素
                int temp = arr[j];
                arr[j] = arr[j+1];
                arr[j+1] = temp;
            }
        }
    }
}

int main() {
    int arr[] = {64, 34, 25, 12, 22, 11, 90};
    int n = sizeof(arr)/sizeof(arr[0]);

    bubbleSort(arr, n);

    std::cout << "Sorted array: \n";
    for (int i = 0; i < n; i++) {
        std::cout << arr[i] << " ";
    }
    std::cout << std::endl;

    return 0;
}

总结

  • 数组是一种存储固定大小相同类型元素的集合。
  • 可以使用forwhile循环遍历数组。
  • 可以通过索引访问和修改数组元素,不能越界访问数组。
  • 静态数组的长度是编译时确定的,可以使用sizeof运算符计算。
  • C++标准数组不支持动态改变大小和直接在数组中插入/删除元素,但可以使用std::vector等动态容器。
  • 可以使用各种排序算法对数组进行排序。

练习

题目1:数组声明与初始化

编写一个程序,声明一个整型数组,并初始化为{1, 3, 5, 7, 9},然后遍历并打印数组中的每个元素。

题目2:数组元素查找

编写一个程序,声明一个整型数组,并初始化为一些随机整数。编写一个函数,该函数接受数组、数组大小和要查找的元素作为参数,如果找到该元素,则返回其在数组中的索引;如果未找到,则返回-1。在main函数中测试该函数。

题目3:数组排序

编写一个程序,声明一个整型数组,并初始化为一些未排序的整数。使用冒泡排序算法对数组进行排序,并打印排序后的数组。

题目4:二维数组操作

编写一个程序,声明并初始化一个二维整型数组(如矩阵),然后编写一个函数,该函数计算并返回矩阵中所有元素的和。在main函数中测试该函数。

题目5:数组与字符串(额外)

虽然字符串在C++中通常通过std::string处理,但了解C风格字符串(字符数组)的处理也是有益的。编写一个程序,声明一个字符数组作为字符串,并使用循环结构遍历字符串,打印出每个字符及其ASCII码值。