指针运算与数组:指针的算术运算,指针与数组的等价关系,数组越界的经典场景

指针和数组,这俩东西在C/C++里就像一对双胞胎。长得像,但性格完全不同。很多新手甚至老手都在这上面栽过跟头。今天我就带你把这层窗户纸捅破。

指针的算术运算:别把它当普通数字

指针加减整数,不是简单的地址加减。它是以「元素大小」为步长的。说白了,ptr + 1 不是地址加1,而是加 sizeof(*ptr) 个字节。

int arr[4] = {10, 20, 30, 40};
int* p = arr;          // p 指向 arr[0]
p = p + 2;             // 实际地址增加了 2 * sizeof(int) = 8 字节
printf("%d\n", *p);    // 输出 30

我个人习惯在调试时,直接打印指针的值和它指向的内容,对比着看。你想想看,如果 p + 1 只加了1个字节,那 int 数组还怎么用?

核心公式:ptr + n 等价于 (char*)ptr + n * sizeof(*ptr)

指针减法同理。两个同类型指针相减,得到的是它们之间相差的元素个数,不是字节数。我在项目中遇到过有人用 (int*)p1 - (int*)p2 算字节偏移,结果数据全乱套了。

指针与数组的等价关系:一个硬币的两面

数组名在大多数表达式中会退化为指向首元素的指针。这就是为什么 arr[i] 和 *(arr + i) 完全等价。

int arr[5] = {1, 2, 3, 4, 5};
// 下面三种写法完全等价
printf("%d\n", arr[2]);       // 3
printf("%d\n", *(arr + 2));   // 3
printf("%d\n", 2[arr]);       // 3  —— 嗯,这也能编译通过

看到 2[arr] 别惊讶。因为 arr[2] 被编译器解释为 *(arr + 2),而加法是可交换的,所以 2[arr] 就是 *(2 + arr)。虽然语法上合法,但我建议你别这么写——代码是给人看的,不是给编译器炫技的。

注意:数组名不是指针变量。它没有自己的存储空间,不能做 arr++ 或 arr = other_ptr 这类操作。数组名是常量地址,指针是变量地址。

数组越界的经典场景:那些年我们踩过的坑

数组越界是C/C++最隐蔽的杀手之一。它不会立刻崩溃,而是悄悄破坏内存,等关键时刻给你致命一击。

场景一:循环边界写错

int arr[5] = {0};
for (int i = 0; i <= 5; i++) {   // 应该是 i < 5
    arr[i] = i * 10;              // i=5 时越界!
}

我曾经在线上服务里排查过一个诡异的内存损坏问题。查了三天,最后发现就是 <= 写成了 <。多写了一个等号,多花了三天时间。嗯,从那以后我写循环都会下意识检查边界。

场景二:字符串忘记预留终止符

char buf[5];
strcpy(buf, "hello");  // "hello" 实际需要6个字节(含'\0')

字符串的 '\0' 是C语言的「潜规则」。你忘了它,它就会在运行时给你「惊喜」。我个人习惯在声明字符数组时,直接写成 char buf[6] = {0};,多一个字节保平安。

场景三:多线程下的越界

多线程环境下的越界更可怕。两个线程操作相邻的数组元素,一个越界写,另一个读到的就是脏数据。而且这种问题极难复现。

// 线程A
int* shared_arr = get_shared_array();
shared_arr[10] = 42;  // 假设数组只有10个元素,这里越界了

// 线程B
int* another_var = get_adjacent_variable();  // 恰好被越界写覆盖
printf("%d\n", *another_var);  // 可能打印42,也可能打印别的

调试技巧:遇到莫名其妙的数据错乱,先检查所有数组访问的边界。可以用 AddressSanitizer(ASan)或 Valgrind 快速定位越界位置。

知识体系图:指针与数组的核心脉络

指针与数组知识体系 指针算术运算 • ptr + n:步长 = sizeof(*ptr) • ptr1 - ptr2:元素个数差 • 自增/自减:移动一个元素 • 比较运算:地址高低判断 数组与指针等价 • arr[i] ⇔ *(arr + i) • 数组名 ≠ 指针变量 • sizeof(arr) 不同 • &arr 与 arr 的区别 数组越界场景 • 循环边界错误 • 字符串缺 '\0' • 多线程竞争越界 • 函数参数传递错误 调试核心:边界检查 + 工具辅助(ASan/Valgrind) 三者环环相扣:理解算术运算 → 掌握等价关系 → 避免越界

避坑指南:我的实战经验

我曾经接手过一个遗留系统,内存泄漏加越界,崩溃日志堆成山。排查过程就像在垃圾堆里找针。最后定位到的问题很简单:一个函数接收了数组指针,但没传数组长度,内部循环越界写坏了堆结构。

从那以后,我给自己定了几条铁律:

  • 传数组必传长度——哪怕你觉得「肯定不会越界」
  • 循环用 < 不用 <=——除非你明确知道要访问边界元素
  • 字符串数组多留一个字节——给 '\0' 留个位置
  • 多线程共享数组加锁或原子操作——别指望巧合

小技巧:调试时可以在数组前后各放一个「哨兵变量」,初始化为固定魔数(比如 0xDEADBEEF)。如果越界写到了哨兵,魔数变了,你一眼就能看出来。

指针和数组的关系,说白了就是「地址」和「连续内存」的舞蹈。你掌握了步长、等价关系和边界意识,大部分坑都能绕过去。剩下的,交给调试工具和耐心吧。

可可精品资料库