一维数组:从定义到实战
数组这东西,说白了就是一块连续的内存空间,里面存了一堆相同类型的数据。我刚开始学C语言时,觉得数组不就是个列表嘛,有啥好讲的?直到后来做嵌入式项目,因为数组越界把整个系统搞崩了,才真正体会到——数组虽简单,坑是真不少。
今天咱们就把一维数组彻底聊透。从怎么定义、怎么初始化,到怎么安全地访问和遍历,最后重点说说那个让人又爱又恨的越界问题。
1. 一维数组的定义
定义数组的语法很简单:
类型 数组名[元素个数];
举个例子:
int scores[5]; // 定义一个能存5个整数的数组
float prices[10]; // 10个浮点数
char name[20]; // 20个字符
这里有个关键点:元素个数必须是常量。你不能写成 int n = 5; int arr[n]; —— 这在C99之前是不允许的。虽然C99引入了变长数组(VLA),但我个人建议别依赖它,移植性太差。
#define MAX_STUDENTS 100,这样改起来方便,代码也更好读。
2. 数组的初始化
数组初始化有好几种写法,我按常用程度给你排个序:
2.1 完全初始化
int arr[5] = {1, 2, 3, 4, 5};
这种最直观,给每个元素都赋了值。
2.2 部分初始化
int arr[5] = {1, 2}; // 等价于 {1, 2, 0, 0, 0}
没赋值的元素自动补0。嗯,这里要注意:只有静态存储期和局部数组的部分初始化才会补0。如果你只声明不初始化,局部数组里的值是垃圾值。
2.3 省略大小的初始化
int arr[] = {1, 2, 3, 4, 5}; // 编译器自动推断大小为5
我特别喜欢这种写法,省得自己数个数。但要注意,一旦你省略了大小,就必须提供初始化列表。
2.4 指定初始化器(C99起)
int arr[10] = {[0] = 10, [5] = 20, [9] = 30};
这个特性我是在做协议解析时用上的。比如要初始化一个状态表,只需要给特定下标赋值,其他保持0,代码清晰多了。
3. 数组元素的访问
访问数组元素用下标运算符 []:
int arr[5] = {10, 20, 30, 40, 50};
printf("%d\n", arr[0]); // 输出10
printf("%d\n", arr[2]); // 输出30
下标从0开始,这是C语言的铁律。我见过不少新手写 arr[1] 想取第一个元素,结果取到第二个,调试半天找不出问题。
其实数组访问的本质是指针运算:arr[i] 等价于 *(arr + i)。所以 arr[0] 就是 *arr,取第一个元素。这个理解透了,后面学指针就轻松了。
arr[i] 但 i 是负数。没错,C语言允许负下标!arr[-1] 访问的是数组前一个地址的内容,这通常不是你想要的。所以一定要确保下标非负且小于数组长度。
4. 数组的遍历
遍历数组最常用的就是for循环:
int arr[5] = {2, 4, 6, 8, 10};
for (int i = 0; i < 5; i++) {
printf("arr[%d] = %d\n", i, arr[i]);
}
输出:
arr[0] = 2
arr[1] = 4
arr[2] = 6
arr[3] = 8
arr[4] = 10
遍历时有个小技巧:用 sizeof 计算数组长度,避免硬编码。
int arr[] = {2, 4, 6, 8, 10};
int len = sizeof(arr) / sizeof(arr[0]);
for (int i = 0; i < len; i++) {
printf("%d ", arr[i]);
}
这样就算以后数组大小变了,遍历代码也不用改。我所有项目里都这么写,省心。
sizeof(arr) 只有在数组定义的作用域内才能得到正确大小。一旦数组作为函数参数传递,它就退化为指针,sizeof 只能得到指针大小(通常是8字节)。所以函数里要额外传一个长度参数。
5. 数组越界问题
这是C语言数组最危险的地方。C语言不检查数组下标是否越界,访问越界内存时,编译器不会报错,但运行时可能引发各种诡异问题。
int arr[3] = {1, 2, 3};
arr[3] = 100; // 越界!arr[3] 不存在
arr[5] = 200; // 越界更远
printf("%d\n", arr[3]); // 可能输出100,也可能崩溃
为什么会这样?因为 arr[3] 访问的是数组后面紧跟着的内存。那块内存里可能存着别的变量,也可能存着函数返回地址。你把它改了,程序行为就不可预测了。
我举个例子,你感受一下:
#include <stdio.h>
int main() {
int a = 10;
int arr[3] = {1, 2, 3};
int b = 20;
arr[3] = 999; // 越界,可能修改了b的值
printf("a = %d, b = %d\n", a, b);
return 0;
}
这段代码在不同编译器、不同优化级别下,输出可能完全不同。有时候 b 变成999,有时候程序直接崩溃。这就是越界的可怕之处——不是每次都会崩溃,但一旦崩溃就是大问题。
recv_len 作为数组下标,没检查 recv_len 是否超过缓冲区大小。结果收到一个恶意构造的超长数据包,直接把栈给踩了,程序跑飞。从那以后,我所有数组访问前都会加边界检查。
安全访问数组的黄金法则:
- 永远确保下标在
[0, 数组长度-1]范围内 - 使用
sizeof计算长度,不要硬编码 - 函数传参时,同时传入数组和长度
- 对用户输入的下标做合法性校验
知识体系总览
下面这张图把一维数组的核心知识点串起来了,你可以对照着复习:
数组这块内容,说白了就三点:怎么存、怎么取、怎么保证安全。定义和初始化解决的是“怎么存”,下标访问和遍历解决的是“怎么取”,而越界问题提醒我们“怎么保证安全”。
我个人觉得,数组是C语言里最基础也最核心的数据结构。它简单,但正因为简单,很多人不重视,结果在越界上栽跟头。记住一句话:数组不检查边界,但你必须检查。
好了,一维数组就聊到这儿。代码多写几遍,边界检查养成习惯,后面学二维数组、指针数组就顺了。