Menu

C 语言数组:声明、初始化与遍历

如何在 C 语言中声明和初始化数组、从零开始索引、用 sizeof 计算长度、遍历元素,以及为什么越过数组末尾读取是未定义行为,而不会给你一条错误信息。

本页包含可运行的编辑器。编辑、运行,立即查看输出。

数组是一块固定大小、由相同类型元素组成的内存,元素在内存中一个接一个地排列。这种连续布局就是全部的关键:正因为如此,索引访问是瞬时的,数组和指针关系如此紧密,C 语言也才能只给你第零个元素的地址,让你通过算术运算找到其余元素。

声明数组

元素类型、名称,再加上方括号里的大小:

int    scores[5];       // 5 ints
double prices[100];     // 100 doubles
char   initials[3];     // 3 chars

大小是元素的个数,必须在声明数组的地方就已知,通常是一个字面量或用 #define 定义的常量。内存会立即预留,之后不会再分配。

刚声明的局部数组里装的是垃圾值,而不是零:

第二行可能打印出零,可能打印出很大的数,而且每次运行、调试版和发布版之间都可能不同。读取这些值是未定义行为;先初始化,再读取。

初始化

有几种写法,都很有用:

最值得记住的是 {0} 这个惯用法:没有列出的元素都会被初始化为零,所以一个零就能把整个数组清零。指定初始化器([4] = 99)让你设置特定位置,其余保持为零,这对大部分为空的查找表很方便。

有一件事做不到:在声明之后再给整个数组赋值:

int a[3] = {1, 2, 3};
int b[3];
b = a;                    // error: an array is not assignable
memcpy(b, a, sizeof a);   // this is how you copy one

索引从零开始

第一个元素是 arr[0],最后一个是 arr[n-1]。不存在 arr[n]。

运行最后那个循环,看看这些地址:每个地址都正好比前一个多 sizeof(int) 个字节。元素确实是首尾相接排列的:

 index      0         1         2         3         4
         +--------+--------+--------+--------+--------+
 value   |   88   |   95   |   75   |   60   |  100   |
         +--------+--------+--------+--------+--------+
 offset   +0       +4       +8       +12      +16      bytes
          ^
          &scores[0], which is also what `scores` decays to

从零开始索引并不是随意的选择:它让索引成为相对起点的偏移量,而这正是地址运算所需要的。scores[i] 被定义为 *(scores + i);详见指针与数组。

求数组长度

C 语言在运行时不会把数组长度存在任何地方,但编译器知道它,而 sizeof 能让你拿到它:

sizeof(arr) / sizeof(arr[0]) 是惯用写法。除以 arr[0] 而不是写死的 sizeof(double),意味着即使你改了元素类型,这行代码也照样有效。

陷阱在于:这只在声明数组的地方有效。把数组传给函数后,形参是一个指针,sizeof 测量的就变成了指针,通常是 8 个字节,得出的“长度”是 1 或 2。这就是为什么 C 语言里每个接收数组的函数都还要接收一个元素个数:

遍历数组

标准写法是一个从 0 开始、条件为 i < n 的 for 循环:

注意是 i < n,不是 i <= n。有 n 个元素时,有效索引是 0 到 n - 1,所以 <= 会多跑一轮,读到末尾之外。用 temps[0] 而不是 0 来初始化 hottest 和 coldest,是另一个值得照搬的习惯:把最大值从零开始算,遇到全是负数的数据时会悄悄出错。

越界是未定义行为

这是 C 语言里最让从其他语言转过来的人吃惊的部分。它没有边界检查。编译时没有,运行时也没有。对一个五个元素的数组访问 arr[10] 并不是错误,它只是编译器一声不吭执行的一次地址计算。

int arr[5] = {1, 2, 3, 4, 5};
arr[7] = 99;     // writes 8 bytes past the end of the array
int x = arr[-1]; // reads before the start

接下来发生的是未定义行为,它的症状五花八门,毫无帮助:

  • 看起来正常运行,因为它碰到的内存恰好没被使用;
  • 另一个变量莫名其妙地变了;
  • 程序因段错误(segmentation fault)崩溃,而且可能是很久以后;
  • 在 -O0 和 -O2 下表现不同。

“看起来正常运行”才是最危险的,因为这意味着 bug 会被发布出去。三种防御方法:

  1. 用 i < n 写循环。大多数越界都是循环条件里的差一错误。
  2. 校验来自输入的索引。
  1. 开发时用 sanitizer 编译。gcc -fsanitize=address -g prog.c 会把大多数越界变成一份即时报告,指出文件、行号和涉及的数组。

其他类型的数组

同样的语法适用于任何元素类型,包括结构体:

char word[6] = "hello"; 这一行值得停下来看看:C 语言的字符串只是一个最后一个有效字节为 '\0' 的 char 数组。正是因为这个结束符,五个字母需要六个位置。

变长数组简介

C99 允许数组的大小是一个运行时的值:

int n = get_count();
int buffer[n];        // a variable-length array

有两点要注意。数组一旦创建,大小就固定了,所以变长数组(VLA)不是一个可以增长的列表。而且它的内存来自栈,如果 n 很大或被攻击者控制,就可能导致栈溢出并让程序崩溃,这也是包括 Linux 内核在内的许多项目直接禁用 VLA 的原因。另外,C11 的实现可以选择不支持它。

当大小确实要到运行时才知道,就改用动态分配:

注意,无论 arr 是数组还是指向已分配内存的指针,arr[i] 的读法完全一样:索引语法并不在乎。详细内容见动态内存。

常见问题

C 语言中如何声明数组?

写出元素类型、名称,再在方括号里写大小:int scores[5]; 为五个 int 预留空间。大小必须是在声明处就已知的常量(变长数组除外),而且除非你提供初始值,否则元素一开始是未初始化的。

C 语言中如何求数组的长度?

用 sizeof(arr) / sizeof(arr[0]):总字节数除以单个元素的大小。它只在声明数组的作用域内有效:数组一旦传给函数,就退化成了指针,sizeof 测量的就变成了指针的大小。

C 语言中越过数组末尾会发生什么?

没有任何东西会阻止你。C 语言不做边界检查,所以对一个 5 个元素的数组访问 arr[10],会读写那个位置上的任意内存。这是未定义行为:可能打印出乱码,可能破坏另一个变量,也可能崩溃,而且它可能一直看起来正常,直到某天出问题。

C 语言中如何把数组的所有元素初始化为零?

写 int arr[100] = {0};:没有列出的元素都会被初始化为零,所以一个零就能把整个数组清零。在 C23 中 int arr[100] = {}; 也可以。如果没有任何初始化器,局部数组的内容就是不确定的垃圾值。

Coddy 编程语言插图

和 Coddy 一起学编程

开始