一维数组:从零开始的数据容器

数组这东西,说白了就是一块连续的内存空间,里面存了一堆相同类型的数据。我刚开始学C语言时,总觉得数组不就是个列表嘛,有啥好讲的?直到后来做嵌入式项目,才发现数组用不好,程序崩得那叫一个惨烈。

1. 一维数组的定义

定义数组的语法很简单:

类型 数组名[元素个数];

举个例子:

int scores[10];      // 存储10个int型数据
float temps[5];      // 存储5个float型数据
char name[20];       // 存储20个char型数据

这里有几个关键点,我当年踩过坑:

  • 元素个数必须是常量——C89标准不支持变量作为数组大小。虽然C99引入了变长数组(VLA),但嵌入式编译器不一定支持。我个人习惯用宏定义:#define MAX_SIZE 100,然后int arr[MAX_SIZE];
  • 数组大小在编译时就确定了——你想想看,数组占用的内存是静态分配的,运行时不能改变大小。
  • 数组名是首元素的地址——这个后面会细说,先记住就行。
⚠️ 注意:数组下标从0开始,不是从1!int a[5]的有效下标是0~4,没有a[5]这个元素。我曾经见过新手写int a[5]; a[5]=100;,结果程序跑飞了。

2. 数组的初始化

初始化数组有几种方式,我按常用程度排个序:

方式一:完全初始化

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

方式二:部分初始化

int arr[5] = {1, 2};  // 等价于 {1, 2, 0, 0, 0}

没赋值的元素自动补0。嗯,这里要注意:只有静态初始化才会补0,局部变量不初始化的话,里面是垃圾值。

方式三:省略大小

int arr[] = {1, 2, 3, 4, 5};  // 编译器自动推断大小为5

这种方式我特别喜欢用,省得数个数。但要注意,如果数组大小由后面决定,就不能再改变用途了。

方式四:指定初始化(C99)

int arr[10] = {[0]=10, [5]=20, [9]=30};

这个在嵌入式开发中偶尔用到,比如初始化寄存器映射表时很方便。

💡 我的习惯:定义数组时尽量初始化,哪怕全赋0。因为局部变量不初始化,里面是随机值,很容易出bug。我曾经在调试一个电机控制程序时,发现数组里莫名其妙有个大数,查了半天才发现是没初始化。

3. 数组元素的访问

访问数组元素用下标运算符[]

int a[5] = {10, 20, 30, 40, 50};
printf("%d\n", a[0]);  // 输出10
printf("%d\n", a[2]);  // 输出30
a[3] = 99;             // 修改第4个元素为99

这里有个底层原理我想说一下:a[i] 实际上等价于 *(a + i)。编译器会计算:a + i * sizeof(int),然后取出那个地址的值。所以下标访问本质上就是指针运算。

有意思的是,你甚至可以写成 i[a],因为 a[i]i[a] 在C语言中是等价的。不过我个人建议别这么写,代码可读性太差了。

4. 数组的越界问题

这是C语言中最常见的bug之一,也是我当年被折磨得最惨的地方。

🚨 越界的后果:C语言不会检查数组下标是否越界!你写 a[10] 编译器不会报错,但运行时可能:
  • 覆盖其他变量的值
  • 破坏栈帧,导致函数返回地址被篡改
  • 触发硬件异常(比如ARM的HardFault)
  • 一切正常——这才是最可怕的,因为bug被隐藏了

我曾经在做一个数据采集项目时,定义了一个uint8_t buffer[64],结果代码里不小心写了个buffer[64] = 0。你猜怎么着?程序跑了三天才崩溃,查bug查得我怀疑人生。后来用内存检测工具才发现是越界写把相邻的变量给覆盖了。

避坑指南:

  • 写循环时,注意边界条件。比如遍历int a[10],循环条件应该是 i < 10,不是 i <= 10
  • 使用宏定义数组大小,不要硬编码数字
  • 接收用户输入时,一定要检查长度,防止缓冲区溢出
  • 嵌入式开发中,可以用静态分析工具(比如PC-Lint)检查越界

5. 数组的遍历

遍历数组最常用的就是for循环:

int arr[5] = {2, 4, 6, 8, 10};
int sum = 0;

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

除了for循环,也可以用while:

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

我个人更推荐for循环,因为循环变量初始化和更新都在一行,不容易漏掉。不过如果你要遍历到某个条件就提前退出,while可能更合适。

还有一种方式是用指针遍历,效率更高:

int *p = arr;
for (int i = 0; i < 5; i++) {
    printf("%d ", *p++);
}

这种方式在嵌入式开发中很常见,因为指针运算比下标运算少一次乘法(虽然现代编译器优化后差别不大)。

知识体系结构图

下面这张图概括了一维数组的核心知识点:

一维数组 定义 类型 数组名[大小] 大小必须是常量 编译时分配内存 初始化 完全初始化 部分初始化(补0) 省略大小(自动推断) 指定初始化(C99) 访问 下标运算符 [] 等价于 *(a+i) 下标从0开始 越界问题 不检查边界 可能覆盖其他变量 遍历 for循环(最常用) while循环 / 指针遍历 应用场景 数据缓存 / 查表 字符串处理 / 状态机

总结一下

数组是C语言中最基础的数据结构,但越简单的东西越容易出错。我总结了三个要点:

  1. 定义时想清楚大小——用宏定义,别硬编码
  2. 访问时检查边界——循环条件写对,别越界
  3. 初始化别偷懒——局部数组不初始化就是定时炸弹

记住这三点,数组这块基本就稳了。下一节我们会讲二维数组,到时候你会发现,一维数组搞明白了,二维数组就是多了一个维度而已。

📌 核心记忆点:数组名是常量指针,不能赋值;数组下标从0开始;C语言不检查越界,全靠程序员自觉。


公众号:蓝海资料掘金营,微信deep3321