一维数组:从定义到实战

数组这东西,说白了就是一块连续的内存空间,里面存了一堆相同类型的数据。我刚开始学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 计算长度,不要硬编码
  • 函数传参时,同时传入数组和长度
  • 对用户输入的下标做合法性校验

知识体系总览

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

一维数组 定义 类型 数组名[常量]; int arr[5]; 初始化 完全/部分/省略大小 指定初始化器 访问 arr[i] 下标从0开始 本质是指针运算 遍历 for循环 + sizeof 函数传参需传长度 越界 不检查下标 后果:数据损坏/崩溃 安全准则 边界检查 不依赖未定义行为 数组是连续内存 + 下标访问 + 边界由程序员负责

数组这块内容,说白了就三点:怎么存、怎么取、怎么保证安全。定义和初始化解决的是“怎么存”,下标访问和遍历解决的是“怎么取”,而越界问题提醒我们“怎么保证安全”。

我个人觉得,数组是C语言里最基础也最核心的数据结构。它简单,但正因为简单,很多人不重视,结果在越界上栽跟头。记住一句话:数组不检查边界,但你必须检查

好了,一维数组就聊到这儿。代码多写几遍,边界检查养成习惯,后面学二维数组、指针数组就顺了。


专注资料整理