C 数组

数组是 C 里最简单的"批量数据"结构——把一组同类型的数据连续存放在内存里,用下标访问。数组是后续学指针、字符串、动态内存的基础,必须吃透。本篇覆盖一维数组、二维数组、数组与指针的关系,以及新手最容易踩的坑:越界访问。

一、一维数组

数组声明语法:类型 名字[长度];。可以在声明时用花括号初始化:

#include <stdio.h>

int main(void) {
    // 1. 声明 + 完全初始化
    int nums[5] = {10, 20, 30, 40, 50};

    // 2. 部分初始化(其余自动补 0)
    int arr[5] = {1, 2};        // 等价 {1, 2, 0, 0, 0}

    // 3. 全部初始化为 0
    int zeros[5] = {0};         // {0, 0, 0, 0, 0}

    // 4. 省略长度,由初始化列表决定
    int xs[] = {1, 2, 3, 4};    // 长度自动是 4

    // 访问:下标从 0 开始
    printf("%d\n", nums[0]);    // 10
    printf("%d\n", nums[4]);    // 50
    nums[2] = 99;               // 修改第 3 个元素

    // 遍历
    int len = sizeof(nums) / sizeof(nums[0]);
    for (int i = 0; i < len; i++) {
        printf("%d ", nums[i]);   // 10 20 99 40 50
    }
    printf("\n");
    return 0;
}

几个要点:

二、越界访问(危险!)

这是 C 数组最致命的特性:C 不做数组边界检查。你访问 arr[5](长度只有 5,有效下标是 0~4),编译器不报错,运行时也不会抛异常,而是默默地读写相邻的内存——可能是其他变量,可能是未分配的内存(段错误),也可能被黑客利用做缓冲区溢出攻击(很多安全漏洞就是这么来的)。

#include <stdio.h>

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

    // 危险!越界访问 - C 不做边界检查!
    printf("%d\n", arr[5]);     // 未定义行为!可能输出垃圾值
    printf("%d\n", arr[100]);   // 极可能崩溃(段错误)

    // 越界写更危险,可能破坏其他变量甚至被利用做攻击
    // arr[5] = 999;   // 经典的"缓冲区溢出"漏洞

    // C 不像 Java/Python 会抛异常,它只是默默地读写相邻内存
    // 这就是为什么很多安全漏洞(缓冲区溢出)出自 C 代码

    // 防御:自己跟踪长度,传数组时一并传长度
    void print_arr(int *a, int n);   // 长度必须显式传
    return 0;
}

防御办法:自己时刻跟踪数组长度,把数组传给函数时同时传长度。这也是为什么 C 标准库的字符串函数(strncpysnprintf)都要你传长度参数。

三、二维数组

二维数组就是"数组的数组",常用来表示矩阵、棋盘、表格:

#include <stdio.h>

int main(void) {
    // 1. 二维数组声明 + 初始化
    int matrix[3][4] = {
        {1, 2, 3, 4},
        {5, 6, 7, 8},
        {9, 10, 11, 12}
    };

    // 2. 也可以连续写
    int m2[2][3] = {1, 2, 3, 4, 5, 6};

    // 3. 部分初始化
    int m3[3][3] = {{1}, {0, 1}, {0, 0, 1}};  // 对角矩阵

    // 访问:matriy[row][col]
    printf("%d\n", matrix[1][2]);   // 7(第2行第3列)

    // 遍历:双层循环
    for (int i = 0; i < 3; i++) {
        for (int j = 0; j < 4; j++) {
            printf("%3d ", matrix[i][j]);
        }
        printf("\n");
    }

    // 行数和列数
    int rows = sizeof(matrix) / sizeof(matrix[0]);       // 3
    int cols = sizeof(matrix[0]) / sizeof(matrix[0][0]); // 4
    printf("%d 行 %d 列\n", rows, cols);
    return 0;
}

二维数组在内存里是按行存储的——matrix[0][0]matrix[0][1]、... matrix[0][3]matrix[1][0] 连续存放。访问 matrix[i][j] 的本质是 *(*(matrix + i) + j)

四、数组作为函数参数

这是新手最容易困惑的点之一。当你把数组传给函数时,实际传递的是首元素指针,不是整个数组的拷贝。这带来两个后果:

#include <stdio.h>

// 数组作为函数参数,实际传递的是"首元素指针"
// 所以函数内无法用 sizeof 求数组长度!
// 必须显式传长度

// 三种等价写法,本质都是 int* arr
void print_arr(int arr[], int n) {
    // sizeof(arr) 在这里是指针大小(8字节),不是数组大小!
    for (int i = 0; i < n; i++) {
        printf("%d ", arr[i]);
    }
    printf("\n");
}

// 另一种写法:显式写指针
void double_each(int *arr, int n) {
    for (int i = 0; i < n; i++) {
        arr[i] *= 2;     // 修改会影响原数组(因为传的是地址)
    }
}

// 二维数组作为参数:第二维大小必须指定
void print_matrix(int rows, int cols, int m[][4]) {
    for (int i = 0; i < rows; i++) {
        for (int j = 0; j < cols; j++) {
            printf("%3d ", m[i][j]);
        }
        printf("\n");
    }
}

int main(void) {
    int nums[] = {1, 2, 3, 4, 5};
    int n = sizeof(nums) / sizeof(nums[0]);

    print_arr(nums, n);          // 1 2 3 4 5
    double_each(nums, n);
    print_arr(nums, n);          // 2 4 6 8 10

    int matrix[2][4] = {{1,2,3,4}, {5,6,7,8}};
    print_matrix(2, 4, matrix);
    return 0;
}

三种写法 int arr[]int arr[10]int *arr 在函数参数里完全等价,都是 int *。推荐第一种或第三种,第二种会误导(让人以为长度被检查了,其实没有)。

五、C99 变长数组 VLA

C99 引入了变长数组(Variable Length Array)——可以用变量作为数组长度:

#include <stdio.h>

// C99 起支持变长数组 VLA(Variable Length Array)
// 长度是运行时变量
int main(void) {
    int n;
    printf("输入数组大小:");
    scanf("%d", &n);

    int arr[n];        // 变长数组,长度由运行时决定
    for (int i = 0; i < n; i++) {
        arr[i] = i * i;
    }
    for (int i = 0; i < n; i++) {
        printf("%d ", arr[i]);
    }
    printf("\n");

    // 注意:VLA 在栈上分配,n 太大会栈溢出
    // 而且 VLA 在 C11 改成可选特性,某些编译器可能不支持
    // 大数组还是建议用 malloc(后续内存篇讲)

    // 实际工程中,变长需求首选 malloc,不用 VLA
    return 0;
}

但 VLA 有争议:

六、数组初始化技巧汇总

七、数组与指针的关系(下篇细讲)

这里先剧透:C 里数组名在很多场景下会"退化"为首元素指针。所以 arr[i] 其实是 *(arr + i) 的语法糖。理解了这一点,你就能理解为什么数组传参后 sizeof 失效、为什么指针能像数组一样用下标访问。下一篇指针会深入讲这个关系。

小结

数组是连续同类型数据的集合,下标从 0 开始。C 不做边界检查,越界访问是未定义行为,自己要管好长度。数组传给函数实际传的是首元素指针,sizeof 失效。二维数组按行存储。下一篇我们看 C 的指针——这是 C 的灵魂,也是数组的进阶。

← 上一篇 C 函数

下一篇 C 指针(重点)

✈️💬