C 语言深度入门系列:第五篇 - 数组:程序世界的数据仓库与批量处理大师

本章目标
本章将深入探讨 C 语言中的数组,这是处理大量同类型数据的核心工具。您将理解数组的内存布局、掌握多维数组的使用,学会字符串操作,并通过经典算法实例领悟数组的强大威力。

1. 核心概念深度剖析

数组 (Array) 的本质:
数组是相同数据类型元素的连续内存块。它不是简单的变量集合,而是一个统一的内存区域,通过索引(下标)来访问其中的元素。

内存布局深度解析:

int numbers[5] = {10, 20, 30, 40, 50};

在内存中的布局:

地址:     1000  1004  1008  1012  1016
内容:     [10]  [20]  [30]  [40]  [50]
索引:      0     1     2     3     4

数组名的真实身份:
数组名numbers实际上是一个指向数组第一个元素的常量指针。这解释了为什么:

  • numbers[0] 等价于 *numbers
  • numbers[i] 等价于 *(numbers + i)
  • 数组名不能被重新赋值

数组与指针的微妙关系:
虽然在很多情况下数组名可以当作指针使用,但它们本质不同:

  • sizeof(array) 返回整个数组的大小
  • sizeof(pointer) 只返回指针本身的大小(通常 8 字节在 64 位系统)

2. 生活化比喻

数组是停车场: 数组就像一个有编号停车位的停车场,每个停车位(元素)只能停放同一类型的车(数据类型),停车位编号从 0 开始,车位是连续排列的。

索引是门牌号: 数组的索引就像门牌号,通过门牌号可以直接找到对应的房间(元素),而不需要挨个搜索。

多维数组是高层建筑: 二维数组像一栋楼,需要楼层号和房间号两个坐标;三维数组像一个小区,需要楼栋号、楼层号、房间号三个坐标。

字符串是珠宝项链: 字符数组组成的字符串就像一串项链,每个珠子(字符)串在一起,最后以特殊的结束标记(‘\0’)作为项链的扣子。

3. 代码与实践:数组的全方位应用

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

int main(void) {
    // 1. 一维数组:声明、初始化、访问
    printf("=== One-Dimensional Arrays ===\n");

    // 多种初始化方式
    int numbers[5] = {10, 20, 30, 40, 50}; // 完全初始化
    int scores[] = {85, 92, 78, 96, 88};   // 自动推断大小
    int grades[10] = {0};                  // 全部初始化为0
    int temperatures[7];                   // 未初始化(包含垃圾值)

    // 数组大小计算
    int array_size = sizeof(numbers) / sizeof(numbers[0]);
    printf("Array size: %d elements\n", array_size);

    // 数组遍历与操作
    printf("Original numbers: ");
    for (int i = 0; i < 5; i++) {
        printf("%d ", numbers[i]);
    }
    printf("\n");

    // 修改数组元素
    numbers[2] = 35; // 修改第3个元素
    printf("After modification: ");
    for (int i = 0; i < 5; i++) {
        printf("%d ", numbers[i]);
    }
    printf("\n");

    // 数组的边界检查(C语言不会自动检查!)
    printf("Dangerous: accessing numbers[10] = %d (undefined behavior!)\n",
           numbers[10]);

    // 2. 多维数组:矩阵和表格
    printf("\n=== Multi-Dimensional Arrays ===\n");

    // 二维数组:行和列
    int matrix[3][4] = {
        {1, 2, 3, 4},
        {5, 6, 7, 8},
        {9, 10, 11, 12}
    };

    // 部分初始化
    int sparse[2][3] = {{1, 2}, {4}}; // 剩余元素自动为0

    printf("3x4 Matrix:\n");
    for (int row = 0; row < 3; row++) {
        for (int col = 0; col < 4; col++) {
            printf("%3d ", matrix[row][col]);
        }
        printf("\n");
    }

    // 内存布局理解:二维数组实际是一维数组的数组
    printf("Matrix memory layout: ");
    int *ptr = (int*)matrix; // 将二维数组当作一维数组
    for (int i = 0; i < 12; i++) {
        printf("%d ", ptr[i]);
    }
    printf("\n");

    // 3. 字符数组与字符串
    printf("\n=== Character Arrays and Strings ===\n");

    // 字符数组的不同初始化方式
    char name1[20] = "Alice";           // 自动添加'\0'
    char name2[] = {'B', 'o', 'b', '\0'}; // 手动添加'\0'
    char name3[20];                     // 未初始化

    // 字符串复制(不能直接赋值!)
    strcpy(name3, "Charlie"); // 使用strcpy函数
    // name3 = "Charlie";     // 错误!不能对数组名赋值

    printf("Name 1: %s (length: %zu)\n", name1, strlen(name1));
    printf("Name 2: %s (length: %zu)\n", name2, strlen(name2));
    printf("Name 3: %s (length: %zu)\n", name3, strlen(name3));

    // 字符串数组(字符指针数组)
    char *fruits[] = {"Apple", "Banana", "Cherry", "Date"};
    int fruit_count = sizeof(fruits) / sizeof(fruits[0]);

    printf("Fruits: ");
    for (int i = 0; i < fruit_count; i++) {
        printf("%s ", fruits[i]);
    }
    printf("\n");

    // 字符串操作函数演示
    char greeting[50] = "Hello, ";
    strcat(greeting, name1);              // 字符串连接
    printf("Greeting: %s\n", greeting);

    if (strcmp(name1, "Alice") == 0) {    // 字符串比较
        printf("Found Alice!\n");
    }

    // 4. 数组作为函数参数
    printf("\n=== Arrays as Function Parameters ===\n");

    // 数组传递给函数时,传递的是指针
    void print_array(int arr[], int size) {
        printf("In function, array size: %zu bytes\n", sizeof(arr)); // 只是指针大小!
        for (int i = 0; i < size; i++) {
            printf("%d ", arr[i]);
        }
        printf("\n");
    }

    printf("In main, array size: %zu bytes\n", sizeof(numbers));
    print_array(numbers, 5);

    // 5. 常用数组算法
    printf("\n=== Common Array Algorithms ===\n");

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

    // 查找最大值和最小值
    int max = data[0], min = data[0];
    int max_index = 0, min_index = 0;

    for (int i = 1; i < data_size; i++) {
        if (data[i] > max) {
            max = data[i];
            max_index = i;
        }
        if (data[i] < min) {
            min = data[i];
            min_index = i;
        }
    }

    printf("Original array: ");
    for (int i = 0; i < data_size; i++) {
        printf("%d ", data[i]);
    }
    printf("\n");
    printf("Max: %d at index %d\n", max, max_index);
    printf("Min: %d at index %d\n", min, min_index);

    // 数组反转
    printf("Reversing array...\n");
    for (int i = 0; i < data_size / 2; i++) {
        int temp = data[i];
        data[i] = data[data_size - 1 - i];
        data[data_size - 1 - i] = temp;
    }

    printf("Reversed array: ");
    for (int i = 0; i < data_size; i++) {
        printf("%d ", data[i]);
    }
    printf("\n");

    // 简单的冒泡排序
    printf("Sorting array...\n");
    for (int i = 0; i < data_size - 1; i++) {
        for (int j = 0; j < data_size - i - 1; j++) {
            if (data[j] > data[j + 1]) {
                int temp = data[j];
                data[j] = data[j + 1];
                data[j + 1] = temp;
            }
        }
    }

    printf("Sorted array: ");
    for (int i = 0; i < data_size; i++) {
        printf("%d ", data[i]);
    }
    printf("\n");

    // 6. 动态数组初探(使用malloc)
    printf("\n=== Dynamic Arrays (Preview) ===\n");

    int size;
    printf("Enter array size: ");
    scanf("%d", &size);

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

    // 初始化动态数组
    for (int i = 0; i < size; i++) {
        dynamic_array[i] = i * i; // 存储平方值
    }

    printf("Dynamic array (squares): ");
    for (int i = 0; i < size; i++) {
        printf("%d ", dynamic_array[i]);
    }
    printf("\n");

    // 释放内存
    free(dynamic_array);
    dynamic_array = NULL; // 防止悬空指针

    return 0;
}

4. 底层原理浅探与常见陷阱

数组的内存分配:

  • 栈数组:局部数组分配在栈上,函数结束时自动释放
  • 静态数组:全局数组或 static 数组分配在数据段,程序结束时释放
  • 动态数组:使用 malloc 分配在堆上,需要手动释放

数组访问的汇编实现:

int arr[10];
int value = arr[3];

汇编代码类似:

mov eax, [base_address + 3*4]  ; base_address + index * sizeof(int)

常见陷阱深度分析:

  1. 数组越界访问:
int arr[5] = {1, 2, 3, 4, 5};
printf("%d\n", arr[10]); // 未定义行为!可能导致段错误或读取垃圾值
  1. 数组退化为指针:
void func(int arr[]) {
    int size = sizeof(arr) / sizeof(arr[0]); // 错误!arr已经是指针
    // 正确做法:将大小作为参数传递
}
  1. 字符串初始化陷阱:
char str[5] = "Hello"; // 错误!需要6个字符(包含'\0')
char str[6] = "Hello"; // 正确
  1. 多维数组参数传递:
void func(int arr[][4]) { } // 正确:必须指定除第一维外的所有维度大小
void func(int arr[][]) { }  // 错误:缺少列数信息
  1. 返回局部数组:
int* create_array() {
    int arr[10] = {0}; // 局部数组
    return arr;        // 错误!返回栈上的地址
}

5. 最佳实践

数组声明与初始化:

  • 总是初始化数组,避免垃圾值
  • 使用 const 修饰只读数组
  • 对于大数组,考虑使用动态分配

边界检查:

  • 编写函数时总是检查数组边界
  • 使用 #define ARRAY_SIZE(arr) (sizeof(arr)/sizeof(arr[0]))

字符串处理:

  • 使用标准库函数(strlen, strcpy, strcat等)
  • 确保目标字符串有足够空间
  • 考虑使用更安全的函数(strncpy, strncat

性能优化:

  • 利用数组的局部性原理,按内存布局顺序访问
  • 对于多维数组,外层循环对应内存中的行

内存管理:

  • 动态分配的数组必须配对 malloc/free
  • 释放后将指针置为 NULL

6. 综合练习

基础练习:

  1. 编写程序,读入 10 个整数,统计其中正数、负数和零的个数。

  2. 实现矩阵的转置:给定一个 3×4 的矩阵,输出其 4×3 的转置矩阵。

进阶练习: 3. 编写一个简单的文本统计程序:

  • 统计一个字符串中每个字符出现的次数
  • 找出出现次数最多的字符
  1. 实现一个简单的学生成绩管理系统:
    • 存储 10 个学生的姓名和成绩
    • 计算平均分,找出最高分和最低分
    • 按成绩排序输出

挑战练习: 5. 实现经典的"生命游戏"(Conway’s Game of Life)的一步演化:

  • 使用二维数组表示细胞状态(0 死 1 活)
  • 根据周围 8 个邻居的状态决定下一代的生死
  1. 编写程序实现简单的图像处理:
    • 使用二维数组表示灰度图像(0-255)
    • 实现图像的 90 度旋转和水平翻转

这篇文章全面覆盖了 C 语言数组的各个方面,从基础概念到高级应用,从内存布局到算法实现,为读者建立了扎实的数组操作基础。

Logo

腾讯云面向开发者汇聚海量精品云计算使用和开发经验,营造开放的云计算技术生态圈。

更多推荐