第 8 章:数组——程序员的储物神器

第 8 章:数组——程序员的储物神器

各位老铁,上一章我们学习了变量和数据类型,那感觉就像学会了造单个砖块。但是!你见过只盖一层楼的房子吗?除非你是霍比特人,否则你肯定需要楼房。同样的,程序世界也需要一种"数据结构",能让你把一堆相关的变量打包存放管理。这就是我们今天的主角——数组(Array)

打个形象的比喻:数组就像是一排整齐的储物柜,每个柜子都有编号,你可以往里面存东西,也可以取出来。每个柜子就是一个元素(Element),柜子的编号就是下标(Index)


8.1 数组的定义与初始化

8.1.1 完全初始化 vs 部分初始化

在 C 语言中,定义数组就像去宜家买收纳柜——你得告诉程序:“我要买多大尺寸的柜子,里面放什么东西。”

完全初始化

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
#include <stdio.h>

int main() {
    // 定义一个包含 5 个整数的数组,并全部赋值
    int scores[5] = {90, 85, 77, 92, 88};

    // 打印所有人的成绩
    for (int i = 0; i < 5; i++) {
        printf("第 %d 个人的成绩是: %d\n", i + 1, scores[i]);
    }

    return 0;
}

输出:

第 1 个人的成绩是: 90
第 2 个人的成绩是: 85
第 3 个人的成绩是: 77
第 4 个人的成绩是: 92
第 5 个人的成绩是: 88

这里 int scores[5] 意思是:“我要 5 个连续的整数存储空间”,然后 = {90, 85, 77, 92, 88} 意思是:“每个柜子里分别放这些分数”。

部分初始化

如果只给部分元素赋值,会发生什么?好奇宝宝们请看:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
#include <stdio.h>

int main() {
    // 只给前3个元素赋值,后2个怎么办?
    int numbers[5] = {10, 20, 30};

    // 打印所有元素看看
    for (int i = 0; i < 5; i++) {
        printf("numbers[%d] = %d\n", i, numbers[i]);
    }

    return 0;
}

输出:

numbers[0] = 10
numbers[1] = 20
numbers[2] = 30
numbers[3] = 0
numbers[4] = 0

未初始化的元素自动被填充为 0!这就像你只往柜子里放了前3个格子的东西,后面的格子自动变成了空的(0)。

这就是 C 语言的**零初始化(Zero Initialization)**特性——局部数组如果不主动赋值,未使用的元素自动归零。省心吧?

当然,如果你想清一色全是 0,可以更偷懒一点:

1
int numbers[5] = {0};  // 只有第一个是0,其他自动全变0

8.1.2 自动计算大小

有时候你懒得数数组里到底有几个元素,比如:

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

编译器会自动帮你数一数,发现有 7 个元素,于是 arr 的长度就是 7。这比自己数然后写 int arr[7] 优雅多了!

不过注意,这种偷懒写法只能在定义的同时初始化时使用。如果你是先声明后赋值,那就不行了:

1
2
int arr[];      // 编译错误:数组定义时必须已知大小
arr = {1, 2, 3}; // 更不可能,数组名不是左值!

8.2 数组下标:arr[0]

好,现在我们知道怎么定义数组了,接下来是如何访问数组中的元素。秘诀就是——下标(Index)

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
#include <stdio.h>

int main() {
    int arr[5] = {100, 200, 300, 400, 500};

    printf("arr[0] = %d\n", arr[0]);
    printf("arr[1] = %d\n", arr[1]);
    printf("arr[2] = %d\n", arr[2]);
    printf("arr[3] = %d\n", arr[3]);
    printf("arr[4] = %d\n", arr[4]);

    return 0;
}

输出:

arr[0] = 100
arr[1] = 200
arr[2] = 300
arr[3] = 400
arr[4] = 500

下标为什么从 0 开始?

这是让无数初学者挠头的问题!为什么不是 arr[1] 是第一个?

答案藏在指针偏移(Pointer Offset)的概念里。数组名 arr 实际上指向数组的首地址,也就是第一个元素的内存位置。

当你写 arr[0] 时,编译器内部把它转换成 *(arr + 0),就是 *arr——解引用首地址,得到第一个元素。

当你写 arr[1] 时,编译器内部把它转换成 *(arr + 1)——跳过一个 int 的大小,再解引用,得到第二个元素。

所以:

  • arr[0] 等价于 *(arr + 0) — 偏移 0 个位置
  • arr[1] 等价于 *(arr + 1) — 偏移 1 个位置
  • arr[2] 等价于 *(arr + 2) — 偏移 2 个位置

![数组下标与指针偏移示意](data:image/svg+xml,%3Csvg xmlns=‘http://www.w3.org/2000/svg' viewBox=‘0 0 600 200’%3E%3Crect x=‘10’ y=‘60’ width=‘80’ height=‘60’ fill=’%233498db’ stroke=’%231a1a1a’ stroke-width=‘2’/%3E%3Ctext x=‘50’ y=‘95’ text-anchor=‘middle’ fill=‘white’ font-family=‘Arial’ font-size=‘14’%3Earr[0]%3C/text%3E%3Ctext x=‘50’ y=‘140’ text-anchor=‘middle’ fill=’%23555555’ font-family=‘Arial’ font-size=‘11’%3E地址: 0x100%3C/text%3E%3Crect x=‘110’ y=‘60’ width=‘80’ height=‘60’ fill=’%233498db’ stroke=’%231a1a1a’ stroke-width=‘2’/%3E%3Ctext x=‘150’ y=‘95’ text-anchor=‘middle’ fill=‘white’ font-family=‘Arial’ font-size=‘14’%3Earr[1]%3C/text%3E%3Ctext x=‘150’ y=‘140’ text-anchor=‘middle’ fill=’%23555555’ font-family=‘Arial’ font-size=‘11’%3E地址: 0x104%3C/text%3E%3Crect x=‘210’ y=‘60’ width=‘80’ height=‘60’ fill=’%233498db’ stroke=’%231a1a1a’ stroke-width=‘2’/%3E%3Ctext x=‘250’ y=‘95’ text-anchor=‘middle’ fill=‘white’ font-family=‘Arial’ font-size=‘14’%3Earr[2]%3C/text%3E%3Ctext x=‘250’ y=‘140’ text-anchor=‘middle’ fill=’%23555555’ font-family=‘Arial’ font-size=‘11’%3E地址: 0x108%3C/text%3E%3Crect x=‘310’ y=‘60’ width=‘80’ height=‘60’ fill=’%233498db’ stroke=’%231a1a1a’ stroke-width=‘2’/%3E%3Ctext x=‘350’ y=‘95’ text-anchor=‘middle’ fill=‘white’ font-family=‘Arial’ font-size=‘14’%3Earr[3]%3C/text%3E%3Ctext x=‘350’ y=‘140’ text-anchor=‘middle’ fill=’%23555555’ font-family=‘Arial’ font-size=‘11’%3E地址: 0x10C%3C/text%3E%3Crect x=‘410’ y=‘60’ width=‘80’ height=‘60’ fill=’%233498db’ stroke=’%231a1a1a’ stroke-width=‘2’/%3E%3Ctext x=‘450’ y=‘95’ text-anchor=‘middle’ fill=‘white’ font-family=‘Arial’ font-size=‘14’%3Earr[4]%3C/text%3E%3Ctext x=‘450’ y=‘140’ text-anchor=‘middle’ fill=’%23555555’ font-family=‘Arial’ font-size=‘11’%3E地址: 0x110%3C/text%3E%3Cpath d=‘M 50 40 L 450 40’ stroke=’%231a1a1a’ stroke-width=‘1’ marker-end=‘url(%23arrow)’/%3E%3Ctext x=‘250’ y=‘35’ text-anchor=‘middle’ fill=’%23333333’ font-family=‘Arial’ font-size=‘12’%3E偏移量: 0 1 2 3 4%3C/text%3E%3C/svg%3E)

想象一下,数组是一排连续的停车位,arr 是第一辆车停的那个位置的门牌号。arr[0] 就是这个位置本身,arr[1] 是往右一个位置,arr[2] 是往右两个位置。如果从 1 开始编号,那每次访问 arr[i] 都要做 arr[i-1] 的转换——多麻烦!计算机最讨厌多做一步运算,所以干脆从 0 开始!


8.3 数组的内存布局:连续存储

数组在内存中是**连续存放(Contiguous Allocation)**的,就像一排紧紧挨着的联排别墅,没有任何缝隙。

假设我们有这样的代码:

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

在内存中大概是这样的:

内存地址(假设从 0x1000 开始,int 占 4 字节):

0x1000  0x1001  0x1002  0x1003 | 0x1004  0x1005  0x1006  0x1007 | ...
   ↓        ↓        ↓        ↓      ↓        ↓        ↓        ↓
┌─────────┬─────────┬─────────┬─────────┬─────────┬─────────┬─────────┐
│  arr[0] │  arr[1] │  arr[2] │  arr[3] │  arr[4] │   ???   │   ???   │
│   10    │   20    │   30    │   40    │   50    │         │         │
└─────────┴─────────┴─────────┴─────────┴─────────┴─────────┴─────────┘
  4字节     4字节     4字节     4字节     4字节

每个 int 占 4 个字节(具体取决于系统),所以每个元素紧紧相连。

这种连续存储的好处是:访问速度超快! CPU 可以直接通过"首地址 + 偏移量 × 元素大小"计算出任何一个元素的位置,就像你知道一排柜子的第一个柜子在哪,然后轻松数到第 N 个柜子。随机访问的时间复杂度是 O(1),也就是常数时间!


8.4 数组名作为指针

这是一个超级重要又容易混淆的概念!让我们来好好掰扯掰扯。

&arr[0]arr 的等价性

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
#include <stdio.h>

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

    printf("arr 的值(首地址):    %p\n", (void*)arr);
    printf("&arr[0] 的值:          %p\n", (void*)&arr[0]);
    printf("两者相等吗? %s\n", arr == &arr[0] ? "YES!" : "NO!");

    // 访问第一个元素的三种等价方式
    printf("\n");
    printf("arr[0] = %d\n", arr[0]);
    printf("*arr   = %d\n", *arr);
    printf("*(&arr[0]) = %d\n", *(&arr[0]));

    return 0;
}

输出:

arr 的值(首地址):    0x7ffd5a3b8c50
&arr[0] 的值:          0x7ffd5a3b8c50
两者相等吗? YES!
arr[0] = 1
*arr   = 1
*(&arr[0]) = 1

所以:

  • arr 的值 = 数组首元素的地址 = &arr[0]
  • *arr = 访问 arr 指向的地址的内容 = arr[0]
  • *(arr + i) = arr[i]

⚠️ 8.4.1 sizeof(arr) 为整个数组大小,不是指针大小

这是面试题和考试中的经典陷阱!请大家注意:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
#include <stdio.h>

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

    printf("sizeof(arr)  = %zu 字节 (整个数组: 5 × 4 = 20 字节)\n", sizeof(arr));
    printf("sizeof(ptr)  = %zu 字节 (指针大小,在64位系统上是8字节)\n", sizeof(ptr));
    printf("sizeof(*ptr) = %zu 字节 (一个 int 的大小)\n", sizeof(*ptr));

    return 0;
}

输出(64 位系统):

sizeof(arr)  = 20 字节 (整个数组: 5 × 4 = 20 字节)
sizeof(ptr)  = 8 字节 (指针大小,在64位系统上是8字节)
sizeof(*ptr) = 4 字节 (一个 int 的大小)

关键区别:sizeof(arr) 中的 arr数组名,代表整个数组,所以返回的是整个数组的字节数(5 × 4 = 20)。而 ptr 只是一个指针变量,它的大小永远是 8 字节(64 位系统)。记住,arr 在大多数情况下会"退化"为指针,但 sizeof 是个例外——它会保留数组的完整大小信息!


8.5 遍历数组

最常用的数组操作之一就是遍历——从头到尾访问每一个元素。

使用 for 循环

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
#include <stdio.h>

int main() {
    int scores[5] = {95, 87, 92, 78, 88};
    int sum = 0;

    printf("=== 全班成绩 ===\n");
    for (int i = 0; i < 5; i++) {
        printf("第 %d 个同学的成绩: %d\n", i + 1, scores[i]);
        sum += scores[i];
    }

    printf("总分: %d,平均分: %.1f\n", sum, sum / 5.0);

    return 0;
}

输出:

=== 全班成绩 ===
第 1 个同学的成绩: 95
第 2 个同学的成绩: 87
第 3 个同学的成绩: 92
第 4 个同学的成绩: 78
第 5 个同学的成绩: 88
总分: 440,平均分: 88.0

使用指针遍历(进阶玩法)

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
#include <stdio.h>

int main() {
    int arr[5] = {10, 20, 30, 40, 50};

    printf("=== 用指针遍历数组 ===\n");
    int *p = arr;  // p 指向数组第一个元素
    for (int i = 0; i < 5; i++) {
        printf("arr[%d] = %d(通过指针: *(p+%d) = %d)\n", i, arr[i], i, *(p + i));
    }

    // 另一种指针遍历方式
    printf("\n=== 指针递增遍历 ===\n");
    p = arr;
    for (int i = 0; i < 5; i++) {
        printf("%d ", *p++);  // 先解引用,再递增指针
    }
    printf("\n");

    return 0;
}

输出:

=== 用指针遍历数组 ===
arr[0] = 10(通过指针: *(p+0) = 10)
arr[1] = 20(通过指针: *(p+1) = 20)
arr[2] = 30(通过指针: *(p+2) = 30)
arr[3] = 40(通过指针: *(p+3) = 40)
arr[4] = 50(通过指针: *(p+4) = 50)

=== 指针递增遍历 ===
10 20 30 40 50

8.6 变长数组(VLA)

C99 引入的变长数组

传统 C 数组的大小必须是常量表达式(编译时就确定)。但 C99 引入了变长数组(Variable Length Array,简称 VLA),允许数组大小在运行时根据变量来确定:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
#include <stdio.h>

int main() {
    printf("请输入要存储的成绩数量: ");
    int n;
    scanf("%d", &n);

    // 数组大小由运行时变量 n 决定
    int scores[n];

    printf("现在请输入 %d 个成绩:\n", n);
    for (int i = 0; i < n; i++) {
        printf("第 %d 个成绩: ", i + 1);
        scanf("%d", &scores[i]);
    }

    printf("\n你输入的成绩是: ");
    for (int i = 0; i < n; i++) {
        printf("%d ", scores[i]);
    }
    printf("\n");

    return 0;
}

输入:

请输入要存储的成绩数量: 3
现在请输入 3 个成绩:
第 1 个成绩: 95
第 2 个成绩: 88
第 3 个成绩: 77

输出:

你输入的成绩是: 95 88 77

这就像你去快递站取包裹,包裹数量是到了才知道的(运行时确定),然后你临时要了对应数量的箱子来装。VLA 就是这样一个"临时工"——数组大小在程序运行时才决定。

C23 中 VLA 成为可选特性

不过要注意,C23 标准将 VLA 标记为可选特性(Optional Feature),也就是说:

  • 在 C99、C11、C17 中,VLA 是标准特性
  • 在 C23 及以后,编译器可以选择不支持 VLA

所以,如果你在面试中被问到 VLA,记得补充一句:“虽然 VLA 很方便,但因为性能和调试问题,C23 已经把它变成可选项了。实际工作中,如果需要动态大小的数组,更推荐使用 malloc 动态分配内存。”


8.7 多维数组:二维数组、三维数组

现实世界往往比一维更复杂。比如:

  • 班级里有多个学生,每个学生有多门成绩 → 二维数组
  • 教学楼里有多个班级,每个班级有多个学生,每个学生有多门成绩 → 三维数组

二维数组的定义与初始化

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
#include <stdio.h>

int main() {
    // 定义一个 3 行 4 列的二维数组(3 个学生,每人 4 门课的成绩)
    int scores[3][4] = {
        {95, 87, 92, 78},   // 第 1 个学生
        {88, 91, 85, 90},   // 第 2 个学生
        {77, 82, 89, 95}    // 第 3 个学生
    };

    // 打印成绩表
    printf("=== 成绩表 ===\n");
    for (int i = 0; i < 3; i++) {
        printf("学生 %d: ", i + 1);
        for (int j = 0; j < 4; j++) {
            printf("%d ", scores[i][j]);
        }
        printf("\n");
    }

    return 0;
}

输出:

=== 成绩表 ===
学生 1: 95 87 92 78
学生 2: 88 91 85 90
学生 3: 77 82 89 95

三维数组的定义

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
#include <stdio.h>

int main() {
    // 定义一个 2 × 3 × 4 的三维数组
    // 可以理解成:2 个班级,每个班级 3 个学生,每个学生 4 门课
    int school[2][3][4] = {
        // 第一个班级(class 0)
        {
            {90, 85, 88, 92},  // 学生 0
            {78, 82, 80, 85},  // 学生 1
            {95, 91, 89, 93}   // 学生 2
        },
        // 第二个班级(class 1)
        {
            {88, 90, 85, 87},  // 学生 0
            {92, 89, 91, 88},  // 学生 1
            {80, 83, 78, 82}   // 学生 2
        }
    };

    // 打印所有成绩
    for (int c = 0; c < 2; c++) {           // 班级
        for (int s = 0; s < 3; s++) {       // 学生
            printf("班级%d-学生%d: ", c + 1, s + 1);
            for (int j = 0; j < 4; j++) {   // 成绩
                printf("%d ", school[c][s][j]);
            }
            printf("\n");
        }
    }

    return 0;
}

输出:

班级1-学生1: 90 85 88 92
班级1-学生2: 78 82 80 85
班级1-学生3: 95 91 89 93
班级2-学生1: 88 90 85 87
班级2-学生2: 92 89 91 88
班级2-学生3: 80 83 78 82

8.8 二维数组的内存布局(行优先存储)

虽然我们写代码时用 scores[3][4] 看起来像表格,但 C 语言在内存中实际是怎么存储的呢?

C 语言采用的是行优先存储(Row-Major Order)。简单来说就是:先填满第一行,再填第二行,再填第三行……

1
2
3
4
int arr[2][3] = {
    {1, 2, 3},   // 第一行
    {4, 5, 6}    // 第二行
};

在内存中实际布局:

逻辑视角(表格):
┌─────────┬─────────┬─────────┐
│  arr[0] │  arr[1] │  arr[2] │  ← 第一行
├─────────┼─────────┼─────────┤
│  arr[3] │  arr[4] │  arr[5] │  ← 第二行
└─────────┴─────────┴─────────┘

内存实际布局(行优先,一维连续):

内存地址:  ... 0x00  0x01  0x02 | 0x03  0x04  0x05 ...
              ↓     ↓     ↓     ↓     ↓     ↓
            ┌────┬────┬────┬────┬────┬────┐
            │ 1  │ 2  │ 3  │ 4  │ 5  │ 6  │
            └────┴────┴────┴────┴────┴────┘
             ↑          ↑
           第一行      第二行

想象成一个停车场:arr[2][3] 就是 2 行 3 列的停车位。停车的时候,先把第一行的 3 个车位停满,再停第二行。内存里就是一维连续排列的 6 辆车。

下面这个程序验证了行优先存储:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
#include <stdio.h>

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

    // 逐个打印元素的地址
    printf("=== 二维数组各元素的内存地址 ===\n");
    for (int i = 0; i < 2; i++) {
        for (int j = 0; j < 3; j++) {
            printf("&arr[%d][%d] = %p → 值: %d\n", i, j, (void*)&arr[i][j], arr[i][j]);
        }
    }

    // 用一维视角打印内存连续性
    printf("\n=== 将二维数组当一维数组用 ===\n");
    int *p = (int *)arr;  // 强制转换为首元素指针
    for (int i = 0; i < 6; i++) {
        printf("p[%d] = %d\n", i, p[i]);
    }

    return 0;
}

输出:

=== 二维数组各元素的内存地址 ===
&arr[0][0] = 0x7ffd5a3b8c50 → 值: 1
&arr[0][1] = 0x7ffd5a3b8c54 → 值: 2
&arr[0][2] = 0x7ffd5a3b8c58 → 值: 3
&arr[1][0] = 0x7ffd5a3b8c5c → 值: 4
&arr[1][1] = 0x7ffd5a3b8c60 → 值: 5
&arr[1][2] = 0x7ffd5a3b8c64 → 值: 6

地址从 0x...c500x...c64,每个 int 占 4 字节,可以清楚看到地址是连续递增的!


8.9 多维数组作为函数参数

把二维数组传给函数时,第二维必须指定。这是为什么呢?

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
#include <stdio.h>

// 打印 3×4 的成绩表
void print_scores(int scores[3][4]) {
    for (int i = 0; i < 3; i++) {
        for (int j = 0; j < 4; j++) {
            printf("%d ", scores[i][j]);
        }
        printf("\n");
    }
}

// 或者用指针的方式(更底层)
void print_scores_ptr(int (*scores)[4], int rows) {
    for (int i = 0; i < rows; i++) {
        for (int j = 0; j < 4; j++) {
            printf("%d ", scores[i][j]);
        }
        printf("\n");
    }
}

int main() {
    int scores[3][4] = {
        {95, 87, 92, 78},
        {88, 91, 85, 90},
        {77, 82, 89, 95}
    };

    printf("=== 第一种写法 ===\n");
    print_scores(scores);

    printf("\n=== 第二种写法(指针+行数参数) ===\n");
    print_scores_ptr(scores, 3);

    return 0;
}

输出:

=== 第一种写法 ===
95 87 92 78
88 91 85 90
77 82 89 95

=== 第二种写法(指针+行数参数) ===
95 87 92 78
88 91 85 90
77 82 89 95

为什么第二维必须指定?因为编译器要知道每一行有多长,才能正确计算 scores[i][j] 在内存中的位置。回顾一下行优先存储,scores[i][j] 的地址 = 首地址 + i × (第二维大小) × 元素大小 + j × 元素大小。如果第二维大小未知,编译器就算不出偏移量,代码就没法工作。

等价的形式还有:

1
2
void func(int scores[][4]) { ... }      // 第一维可以省略
void func(int (*scores)[4]) { ... }     // 数组指针语法糖

8.10 常见错误

错误一:数组越界

这是 C 语言中最危险也最常见的错误之一!

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
#include <stdio.h>

int main() {
    int arr[3] = {1, 2, 3};  // 只能访问 arr[0] 到 arr[2]

    printf("正常访问: arr[0] = %d\n", arr[0]);
    printf("正常访问: arr[2] = %d\n", arr[2]);

    // ⚠️ 危险!越界访问
    printf("越界访问: arr[3] = %d\n", arr[3]);  // 未定义行为!
    printf("越界访问: arr[100] = %d\n", arr[100]);  // 更危险!

    return 0;
}

数组越界(Out of Bounds)就像你闯入了邻居的房子。编译器不会阻止你,程序可能看起来正常运行(读到了其他变量的值,或者刚好那片内存没人用),但更多时候会导致程序崩溃、数据损坏,甚至被黑客利用(缓冲区溢出攻击)。C 语言不检查数组越界,程序员自己要心里有数!

错误二:数组名作为左值

1
2
3
4
5
6
int arr[5] = {1, 2, 3, 4, 5};
int brr[5] = {10, 20, 30, 40, 50};

// ⚠️ 错误写法!
arr = brr;  // 编译错误!数组名不是可修改的左值
arr++;      // 编译错误!数组名不能自增

数组名 arr 在表达式中会退化(Decay)为指向首元素的指针,但这个指针是常量指针——你不能改变它的指向,也不能把它赋值给另一个指针变量(虽然指针本身的值可以赋给另一个指针)。就像一个房间的门牌号,你能用它找到房间,但你不能把门牌号改成另一个地址。

正确做法——用 memcpy 或循环复制:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
#include <stdio.h>
#include <string.h>

int main() {
    int arr[5] = {1, 2, 3, 4, 5};
    int brr[5] = {10, 20, 30, 40, 50};

    // 正确做法:用 memcpy 复制
    memcpy(arr, brr, sizeof(arr));

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

    return 0;
}

输出:

arr[0] = 10
arr[1] = 20
arr[2] = 30
arr[3] = 40
arr[4] = 50

错误三:sizeof 误用

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
#include <stdio.h>

void print_array(int arr[]) {
    // ⚠️ 错误:想用 sizeof 计算元素个数
    int count = sizeof(arr) / sizeof(arr[0]);  // 错误!

    // 正确做法:额外传入数组长度
    // print_array(arr, 5);
}

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

    // 正确:在 main 中使用 sizeof
    int count = sizeof(arr) / sizeof(arr[0]);
    printf("元素个数: %d\n", count);  // 输出 5,正确!

    print_array(arr);

    return 0;
}

函数参数中的 arr[] 实际上是一个指针!当数组作为函数参数传递时,它会退化为指针,丢失了数组的长度信息。所以 sizeof(arr) 在函数内部只能得到指针的大小(8 字节),而不是整个数组的大小。这就是为什么 C 数组传给函数时,必须额外传递一个长度参数的原因。


本章小结

  1. 数组是相同类型元素的连续存储结构,通过下标访问,下标从 0 开始(因为指针偏移)。

  2. 部分初始化的数组,未赋值的元素自动初始化为 0。

  3. 编译器可以自动计算数组大小int arr[] = {1, 2, 3} 会自动创建一个长度为 3 的数组。

  4. 数组名在表达式中退化为指针,但 sizeof(arr) 保留数组完整大小。

  5. **变长数组(VLA)**在 C99 引入,C23 中成为可选特性。

  6. 二维数组按行优先存储,多维数组作为函数参数时,除了第一维可以省略,其他维度必须指定大小。

  7. 数组越界是未定义行为,可能导致程序崩溃或安全漏洞;数组名不能作为左值赋值;sizeof 在函数参数中会退化为指针大小而非数组大小。

数组是 C 语言中最基础也是最重要的数据结构。掌握了数组,你就掌握了程序世界中"批量处理数据"的基本功。下一章我们将学习字符串——一种用数组存放的特殊数据类型,敬请期待!

最后修改 March 29, 2026: 新增 C 教程 (93a26d7)