8. 数组(一):一维数组——从定义到实战
数组这东西,说白了就是一块连续的内存空间,里面存了一堆相同类型的数据。我刚开始学C语言那会儿,觉得数组不就是个列表嘛,有啥好学的?后来做项目才发现,数组用得好不好,直接决定了代码的优雅程度和运行效率。
今天咱们就聊聊一维数组。别小看它,这可是所有复杂数据结构的基石。
8.1 一维数组的定义与初始化
先看定义格式,其实很简单:
类型说明符 数组名[常量表达式];
举个例子:
int scores[10]; // 定义一个能存10个整数的数组
float prices[5]; // 5个浮点数
char name[20]; // 20个字符
这里有个坑,我当年就踩过——数组长度必须是常量。你不能写成:
int n = 10;
int arr[n]; // 这在C89标准里是错的!
初始化方式有几种,我列出来给你看:
// 方式1:完全初始化
int a[5] = {1, 2, 3, 4, 5};
// 方式2:部分初始化,未指定的元素自动补0
int b[5] = {1, 2}; // 等价于 {1, 2, 0, 0, 0}
// 方式3:不指定长度,由编译器自动计算
int c[] = {1, 2, 3, 4, 5}; // 长度为5
// 方式4:全部初始化为0
int d[5] = {0}; // 最常用的写法
我个人习惯用方式4来清零数组。你想想看,如果忘了初始化就直接用数组,里面的值可是随机的——这就是所谓的"野值"。我在项目中遇到过好几次,就是因为局部数组没初始化,导致程序行为时好时坏,排查了半天才发现。
8.2 数组元素的访问
数组元素的访问靠下标,下标从0开始。这个规则估计你早就知道了,但为什么从0开始?嗯,这里有个历史原因——数组名其实是个指针常量,指向第一个元素。a[i] 在编译器眼里就是 *(a + i)。如果从1开始,那每次访问都要做 *(a + i - 1),多一次减法运算。
访问方式很简单:
int arr[5] = {10, 20, 30, 40, 50};
printf("%d\n", arr[0]); // 输出10
printf("%d\n", arr[2]); // 输出30
arr[3] = 100; // 修改第4个元素为100
越界访问是个大问题。我曾经调试过一个bug,程序偶尔崩溃,偶尔正常。最后发现是数组越界写,把别的变量给覆盖了。这种bug最难查,因为错误发生的地方和症状表现的地方往往不在同一处。
8.3 数组的遍历
遍历数组,最常用的就是for循环。说白了就是挨个访问每个元素:
int arr[5] = {2, 4, 6, 8, 10};
// 正向遍历
for (int i = 0; i < 5; i++) {
printf("arr[%d] = %d\n", i, arr[i]);
}
// 反向遍历
for (int i = 4; i >= 0; i--) {
printf("arr[%d] = %d\n", i, arr[i]);
}
这里有个小技巧——用sizeof计算数组长度:
int arr[] = {1, 3, 5, 7, 9, 11};
int len = sizeof(arr) / sizeof(arr[0]); // 6
for (int i = 0; i < len; i++) {
printf("%d ", arr[i]);
}
为什么这样能算出来?因为 sizeof(arr) 是整个数组占的字节数,sizeof(arr[0]) 是一个元素占的字节数,一除就是元素个数。这个写法我几乎每天都在用,比硬编码长度靠谱多了。
8.4 数组作为函数参数
数组作为函数参数时,有个重要的特性——传递的是地址,不是整个数组的副本。这意味着在函数内部修改数组元素,会直接影响原数组。
void printArray(int arr[], int len) {
for (int i = 0; i < len; i++) {
printf("%d ", arr[i]);
}
printf("\n");
}
void doubleArray(int arr[], int len) {
for (int i = 0; i < len; i++) {
arr[i] *= 2; // 直接修改原数组
}
}
int main() {
int scores[] = {60, 70, 80, 90, 100};
int len = sizeof(scores) / sizeof(scores[0]);
printArray(scores, len); // 输出:60 70 80 90 100
doubleArray(scores, len); // 每个元素翻倍
printArray(scores, len); // 输出:120 140 160 180 200
return 0;
}
你看,doubleArray 函数里改了数组,main函数里的数组也跟着变了。这就是传地址的效果。
这里有个细节——为什么还要传长度参数? 因为数组作为参数退化成了指针,函数内部无法通过sizeof获取数组长度。所以必须把长度一起传进去。
我曾经见过一个新手写的代码:
void badFunction(int arr[]) {
int len = sizeof(arr) / sizeof(arr[0]); // 错误!arr是指针
// ...
}
这个len算出来永远是1(在32位系统上,指针4字节,int也是4字节)。嗯,这种bug特别隐蔽,因为代码看起来没问题,但结果就是不对。
知识体系总览
下面这张图把一维数组的核心知识点串起来了,你可以对照着复习:
避坑指南
最后,我把这些年踩过的坑总结一下:
- 数组越界:C语言不检查边界,越界了也不报错。但后果很严重——可能覆盖其他变量,也可能导致段错误。写循环时一定要确认边界条件。
- 未初始化:局部数组不会自动清零,里面的值是随机的。我建议你养成习惯,定义时就初始化,哪怕只是
= {0}。 - 函数传参丢失长度信息:数组传给函数后,sizeof就失效了。记得把长度作为单独的参数传进去。
- 用变量定义数组长度:在C89标准下不行,C99虽然支持VLA,但移植性差。为了保险,还是用常量吧。
数组这东西,看着简单,但用好了能写出很优雅的代码。我见过有人用数组实现循环队列、哈希表、甚至简单的内存池——这些都是后话了。先把一维数组的基础打牢,后面的路就好走了。
公众号:蓝海资料掘金营,微信deep3321