第 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 个位置
flowchart LR
    A0["arr[0]<br/>地址 0x100<br/>偏移 0"] --- A1["arr[1]<br/>地址 0x104<br/>偏移 1"] --- A2["arr[2]<br/>地址 0x108<br/>偏移 2"] --- A3["arr[3]<br/>地址 0x10C<br/>偏移 3"] --- A4["arr[4]<br/>地址 0x110<br/>偏移 4"]

想象一下,数组是一排连续的停车位,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 就是这样一个"临时工"——数组大小在程序运行时才决定。

VLA 是"可选特性"(C11 起)

不过在依赖 VLA 之前,有几个坑必须知道:

  • C99 中 VLA 是必须支持的标准特性;
  • 从 C11 起,VLA 变成了条件特性(conditional feature)——编译器可以选择不支持,此时它会定义宏 __STDC_NO_VLA__ 为 1。C23 延续了这一安排(N3096 明确写着"VLA 是实现不必支持的条件特性")。

所以在写可移植代码时,可以用这个宏来"探测":

1
2
3
#if defined(__STDC_NO_VLA__)
#error "这个编译器不支持变长数组,请改用 malloc"
#endif

另外两个现实问题:

  1. 栈空间有限:VLA 分配在栈上(自动存储期),如果 n 很大(比如用户输入 1000000),栈会直接溢出崩溃。
  2. sizeof 变成运行时求值,调试器和某些优化也会变得更复杂。

如果你在面试中被问到 VLA,可以这样答:“VLA 从 C11 起就是可选特性,编译器可以用 __STDC_NO_VLA__ 声明不支持;而且它分配在栈上、大小不可控,实际工程里需要动态大小数组时更推荐用 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...c50 到 0x...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)为指向首元素的指针(int *)。但这里的 arr 不是一个可以修改的指针变量,而是一个"指向固定位置的符号":你不能 arr = brr(不能让它改指向别处),也不能 arr++。换言之,你可以读出 arr 的值赋给别的指针变量(比如 int *p = arr;),但不能给 arr 本身赋值。就像一个房间的门牌号:你能照着它找到房间,也能把号码抄到别处,但不能把门牌号改成另一个地址。

正确做法——用 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
22
23
24
25
26
27
28
29
30
31
#include <stdio.h>

/* ⚠️ 错误示例:在函数内部用 sizeof 数元素个数 */
void print_array_wrong(int arr[]) {
    /* 此时 arr 已经退化成 int*,sizeof(arr) 只是指针大小(8 字节)!
       sizeof(arr[0]) 是 4,所以算出来是 8/4 = 2,完全错误。 */
    size_t count = sizeof(arr) / sizeof(arr[0]);
    printf("函数里算出的“元素个数”: %zu(错的!)\n", count);
}

/* ✅ 正确做法:把长度一起传进来 */
void print_array(int arr[], size_t n) {
    printf("真正的元素个数: %zu\n", n);
    for (size_t i = 0; i < n; i++) {
        printf("%d ", arr[i]);
    }
    printf("\n");
}

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

    // 在定义数组的同一个作用域里,sizeof(arr) 才是整个数组的字节数
    size_t count = sizeof(arr) / sizeof(arr[0]);
    printf("main 里算出的元素个数: %zu(正确)\n", count);

    print_array_wrong(arr);   // 看一下错误的结果
    print_array(arr, count);  // 正确做法

    return 0;
}

在某台 64 位机器上会看到:

main 里算出的元素个数: 5(正确)
函数里算出的“元素个数”: 2(错的!)
真正的元素个数: 5
1 2 3 4 5

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


本章小结

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

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

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

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

  5. **变长数组(VLA)**在 C99 引入;从 C11 起它是"条件特性"(编译器可以定义 __STDC_NO_VLA__ 表示不支持),C23 延续了这一安排。

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

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

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