8. 数组(一):一维数组——从定义到实战

数组这东西,说白了就是一块连续的内存空间,里面存了一堆相同类型的数据。我刚开始学C语言那会儿,觉得数组不就是个列表嘛,有啥好学的?后来做项目才发现,数组用得好不好,直接决定了代码的优雅程度和运行效率。

今天咱们就聊聊一维数组。别小看它,这可是所有复杂数据结构的基石。

8.1 一维数组的定义与初始化

先看定义格式,其实很简单:

类型说明符 数组名[常量表达式];

举个例子:

int scores[10];      // 定义一个能存10个整数的数组
float prices[5];     // 5个浮点数
char name[20];       // 20个字符

这里有个坑,我当年就踩过——数组长度必须是常量。你不能写成:

int n = 10;
int arr[n];  // 这在C89标准里是错的!
注意: 虽然C99支持了变长数组(VLA),但很多嵌入式编译器不支持。为了代码的可移植性,我建议你还是老老实实用常量定义数组长度。

初始化方式有几种,我列出来给你看:

// 方式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
核心要点: 数组下标从0开始,最大下标是长度-1。访问 arr[5] 就是越界了,编译器不会报错,但运行时可能崩溃。

越界访问是个大问题。我曾经调试过一个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]) 是一个元素占的字节数,一除就是元素个数。这个写法我几乎每天都在用,比硬编码长度靠谱多了。

小提示: 这个技巧只能在数组定义的作用域内使用。如果把数组传给函数,sizeof(arr) 就变成指针的大小了,算出来就不对了。

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特别隐蔽,因为代码看起来没问题,但结果就是不对。

知识体系总览

下面这张图把一维数组的核心知识点串起来了,你可以对照着复习:

一维数组知识体系 一维数组 定义与初始化 类型 数组名[长度]; int a[5] = {1,2,3}; int b[] = {1,2,3}; int c[5] = {0}; 元素访问 下标从0开始 arr[0] 第一个元素 arr[i] = *(arr + i) ⚠ 注意越界 数组遍历 for循环遍历 正向/反向遍历 sizeof求长度 len = sizeof(arr)/sizeof(arr[0]) 作为函数参数 传地址,不传副本 需额外传长度参数

避坑指南

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

  • 数组越界:C语言不检查边界,越界了也不报错。但后果很严重——可能覆盖其他变量,也可能导致段错误。写循环时一定要确认边界条件。
  • 未初始化:局部数组不会自动清零,里面的值是随机的。我建议你养成习惯,定义时就初始化,哪怕只是 = {0}
  • 函数传参丢失长度信息:数组传给函数后,sizeof就失效了。记得把长度作为单独的参数传进去。
  • 用变量定义数组长度:在C89标准下不行,C99虽然支持VLA,但移植性差。为了保险,还是用常量吧。

数组这东西,看着简单,但用好了能写出很优雅的代码。我见过有人用数组实现循环队列、哈希表、甚至简单的内存池——这些都是后话了。先把一维数组的基础打牢,后面的路就好走了。


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