第12章:数组基础——一维数组的定义、初始化、访问,数组的越界问题
数组这东西,说白了就是一块连续的内存空间,里面存了一堆相同类型的数据。我刚开始学C语言时,觉得数组不就是个列表嘛,有啥好讲的?直到后来在项目中踩了越界的坑,才明白这玩意儿看似简单,实则暗藏杀机。
12.1 一维数组的定义
定义数组的语法很简单:
类型 数组名[元素个数];
举个例子:
int scores[10]; // 定义一个能存10个整数的数组
float prices[5]; // 定义一个能存5个浮点数的数组
char name[20]; // 定义一个能存20个字符的数组
这里有个关键点——数组的大小必须是常量。C99之前,你不能写成 int n = 10; int arr[n];,这叫变长数组(VLA),虽然C99支持了,但很多嵌入式编译器压根不认。我个人习惯在嵌入式开发中一律用宏定义:
#define MAX_STUDENTS 50
int student_ids[MAX_STUDENTS];
这样改起来方便,代码也清晰。
12.2 数组的初始化
数组初始化有好几种玩法,我一个个说。
12.2.1 完全初始化
int arr[5] = {1, 2, 3, 4, 5};
把5个元素全给填上,明明白白。
12.2.2 部分初始化
int arr[5] = {1, 2};
只给了前两个值,剩下的怎么办?C语言规定:未显式初始化的元素自动赋值为0。所以上面这个数组实际上是 {1, 2, 0, 0, 0}。
int arr[100] = {0};。第一个元素赋0,剩下的自动补0,省事。
12.2.3 不指定大小的初始化
int arr[] = {10, 20, 30, 40};
编译器会自动数一下,知道你有4个元素,所以数组大小就是4。我在项目中经常用这种方式,尤其是定义查表用的常量数组时:
const int days_in_month[] = {31, 28, 31, 30, 31, 30, 31, 31, 30, 31, 30, 31};
这样加月份也不用改大小,编译器帮你算。
12.3 数组的访问
数组通过下标访问,下标从0开始。比如:
int arr[5] = {10, 20, 30, 40, 50};
printf("%d\n", arr[0]); // 输出10
printf("%d\n", arr[2]); // 输出30
arr[4] = 99; // 把最后一个元素改成99
这里有个容易混淆的点——arr[0]是第一个元素,arr[4]是第五个元素。你想想看,定义时写的是 int arr[5],但能访问的下标范围是0到4,不是1到5。我见过不少新手在这上面栽跟头。
12.4 数组的越界问题
好,重点来了。数组越界,可以说是C语言里最臭名昭著的坑之一。
arr[10] 编译器不会报错,但运行时可能直接崩掉。
为什么会这样?因为C语言追求效率,运行时检查下标会拖慢速度。所以它把责任交给了程序员——你自己管好自己。
越界访问会发生什么?
- 读取越界: 拿到的是那块内存上的垃圾值,可能是别的变量,可能是函数返回地址,鬼知道是什么。
- 写入越界: 更危险。你改写了别人的内存,可能把别的变量值改了,可能把函数指针改了,程序行为变得诡异。
我曾经在一个项目中遇到过这样的bug:一个嵌入式设备偶尔死机,查了三天,最后发现是数组越界把任务栈给踩了。从那以后,我写循环遍历数组时,一定会用 sizeof 算好边界:
int arr[100];
for (int i = 0; i < sizeof(arr) / sizeof(arr[0]); i++) {
arr[i] = i * 2;
}
sizeof(arr) 是整个数组占的字节数,sizeof(arr[0]) 是一个元素占的字节数,一除就是元素个数。这样就算以后改了数组大小,循环也不用改。
12.5 数组与指针的暧昧关系
数组名其实是个指针常量,指向数组的第一个元素。所以:
int arr[5] = {1, 2, 3, 4, 5};
printf("%p\n", arr); // 打印数组首地址
printf("%d\n", *arr); // 输出1,等价于 arr[0]
printf("%d\n", *(arr+2)); // 输出3,等价于 arr[2]
但注意,arr 是常量,你不能写 arr++。如果你需要移动指针,得另外定义一个指针变量:
int *p = arr;
p++; // 合法,p指向arr[1]
嗯,这里要提醒一下:sizeof(arr) 和 sizeof(p) 的结果不一样。前者是整个数组的大小,后者只是一个指针的大小(通常是4或8字节)。
12.6 知识体系图
下面这张图把本章的核心内容串起来了:
12.7 避坑指南
最后,我把自己这些年踩过的坑总结一下:
我曾经... 在写一个通信协议解析函数时,用 for (i = 0; i <= N; i++) 遍历数组,结果多了一次越界写入,把紧挨着的校验和变量给改了。整整排查了两天,最后发现是 <= 写成了 <。从那以后,我写循环一律用 <,绝不偷懒。
还有几个要点:
- 函数传参时数组会退化为指针,所以在函数内部用
sizeof(arr)拿不到数组大小,只能拿到指针大小。这时候必须把长度也传进去。 - 不要用变量做数组大小(除非你的编译器支持VLA且你确认需要),否则移植性很差。
- 字符串数组记得留一个位置给 '\0',比如存"hello"需要
char str[6],不是5。
数组是C语言的基石,用好了事半功倍,用不好就是定时炸弹。多写、多练、多注意边界,慢慢就形成肌肉记忆了。
公众号:蓝海资料掘金营,微信deep3321