第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语言里最臭名昭著的坑之一。

警告: 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 知识体系图

下面这张图把本章的核心内容串起来了:

一维数组 定义 类型 数组名[常量]; int arr[10]; 初始化 完全 / 部分 / 不指定大小 int a[5]={1,2}; // 余下补0 访问 下标从0开始 arr[0] 第一个元素 越界问题 C语言不检查下标 读取垃圾值 / 写入破坏 数组名 = 指针常量 arr 指向首元素 *(arr+2) 等价于 arr[2] 避坑指南 用sizeof算边界 循环时注意下标范围

12.7 避坑指南

最后,我把自己这些年踩过的坑总结一下:

我曾经... 在写一个通信协议解析函数时,用 for (i = 0; i <= N; i++) 遍历数组,结果多了一次越界写入,把紧挨着的校验和变量给改了。整整排查了两天,最后发现是 <= 写成了 <。从那以后,我写循环一律用 <,绝不偷懒。

还有几个要点:

  • 函数传参时数组会退化为指针,所以在函数内部用 sizeof(arr) 拿不到数组大小,只能拿到指针大小。这时候必须把长度也传进去。
  • 不要用变量做数组大小(除非你的编译器支持VLA且你确认需要),否则移植性很差。
  • 字符串数组记得留一个位置给 '\0',比如存"hello"需要 char str[6],不是5。

数组是C语言的基石,用好了事半功倍,用不好就是定时炸弹。多写、多练、多注意边界,慢慢就形成肌肉记忆了。


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