本节摘要:数组 = 一块连续内存 + 元素宽度,下标访问编译成"首地址 + 下标乘宽度"一条指令,O(1) 且缓存友好——代价是机器完全不设防:越界访问的指令与正常访问一模一样,踩到谁全看隔壁住着谁。本节看数组的字节形态与初始化规则,然后制造几起真实的越界事故,学习从废墟里读出线索。
#include <stdio.h> int main(void) { int a[5] = {10, 20, 30, 40, 50}; printf("sizeof a = %zu\n", sizeof a); /* 20 */ printf("a 的地址 %p\n", (void *)a); printf("&a[1] 与 a 相差 %td 字节\n", (char *)&a[1] - (char *)a); /* 4 */ return 0; }
五个 int 紧挨着,共 20 字节,地址步进 4。第 5 章已经推过公式:元素地址 = 首地址 + 下标 × 4,编译器把它直接嵌进访存指令的缩放因子。初始化规则补全:
int a[5] = {1, 2, 3}; /* 剩余元素自动清零:4、5 号是 0 */ int b[] = {1, 2, 3}; /* 长度由初始化列表推得:3 */ int c[5] = {0}; /* 全零数组的惯用写法 */ /* int d[2] = {1,2,3}; 编译错误:塞不下 */
注意"部分初始化则剩余清零"这条规则只对数组与全局变量成立——普通局部变量不初始化仍是垃圾值(第 4 章)。数组没有内置长度信息,sizeof 只在未退化的场合可用,跨函数必须显式传长度——这条铁律值得再背一遍。
#include <stdio.h> int main(void) { int guard1 = 111; int a[2] = {0, 0}; int guard2 = 222; a[2] = 999; /* 越界写:只有 0 和 1 合法 */ a[-1] = 888; /* 反向越界 */ printf("guard1 = %d\n", guard1); /* 888:被反向越界踩了 */ printf("guard2 = %d\n", guard2); /* 999:被正向越界踩了 */ return 0; }
这段代码在很多编译器上能"正常"跑完并打印出被踩后的值——没有报错、没有崩溃、数据悄悄换掉了。编译器可能把三个变量排成相邻(也可能调整顺序或加填充,所以结果因编译器而异),越界写直接落在邻居变量上。这就是越界最可怕的一面:它是无声的。等到崩溃时,案发现场早已被踩乱。

| 越界类型 | 典型肇因 | 常见下场 |
|---|---|---|
| 栈上数组越界 | 循环边界差一、scanf %s 超长 | 踩局部变量或返回地址;踩返回地址可被利用执行任意代码 |
| 堆块越界 | malloc 大小算错、strcpy 不限长 | 踩 malloc 元数据,堆损坏,崩在很远处的 free |
| 全局数组越界 | 下标来自外部输入未检查 | 踩其他全局变量,或掉出数据段段错误 |
栈上越界踩返回地址值得单独强调:精心构造的超长输入可以把返回地址覆盖成攻击者指定的位置——缓冲区溢出攻击的原理至此已经完整可见(第 2 章 scanf 一节说的 %19s 限长,防的就是它)。
一个体现"长度显式化"的规范函数骨架:
int sum_array(const int *a, size_t n) /* 长度是参数,责任清晰 */ { int s = 0; for (size_t i = 0; i < n; i++) /* 循环上界与长度绑定 */ s += a[i]; return s; }
⚠️ 常见坑:差一错误(off-by-one)。
for (i = 0; i <= n; i++)对长度 n 的数组多访问一个元素;边界条件写<=前多看一眼。另一个高频坑是"长度传了元素个数,循环里当下标用还减一"——长度与最后下标差一,这条要形成肌肉记忆。
💡 关键直觉:数组是效率与安全的交易——用"不检查边界"换来 O(1) 访问与缓存友好。C 把检查的责任完全交给你,所以纪律必须自己建:长度永远显式传,下标永远来自受控来源。
下一节把一维数组卷成网格:二维数组的内存真相与遍历顺序的性能差。
问:数组越界为什么有时不崩溃? 因为访问指令本身不检查边界,后果取决于踩到谁。踩到普通变量只是数据错乱,踩到只读页或未映射页才崩溃。无声的越界是最危险的越界,排查时不能依赖"崩了才说明有问题"。
问:长度为什么不能存进数组里? C 数组只是内存与首地址,没有头部元数据可存长度。语言设计者把检查权交给程序员,换来的是零管理开销。要安全就自己传长度,或封装带长度的结构。
问:越界检测工具会拖慢多少? 地址消毒器大约拖慢两倍并占用更多内存,只在测试构建开启。它是发现越界性价比最高的手段,测试跑得慢一点换事故当场现形,完全值得。