第 3 章 · 指针运算

指针加减按「元素个数」缩放、不是按字节——这是 C 指针独有的能力,也是 bug 之源。

2.1 指针加减整数:按元素缩放

int a[5] = {10, 20, 30, 40, 50};
int *p = &a[0];        // 或 int *p = a;

p + 1;   // 不是地址 +1,而是地址 + sizeof(int) = +4 字节
p + 2;   // +8 字节
p - 1;   // -4 字节(前提:结果仍在同一数组/对象内)

p + n 的语义:指向「从 p 开始往后数第 n 个元素」的那个位置。编译器自动乘上 sizeof(*p)

p + n   ≡   (int*)((char*)p + n * sizeof(*p))

所以 char* 加 1 前进 1 字节,double* 加 1 前进 8 字节——缩放因子就是指向类型的大小。

2.2 p[i] 就是 *(p+i)

C 里下标运算符本质是指针运算的语法糖,二者完全等价

p[i]   ≡   *(p + i)   ≡   *(i + p)   ≡   i[p]   // 连 i[p] 都合法!

正因如此,数组下标从 0 开始是必然的:a[0] 就是 *(a + 0),即首元素本身,没有额外偏移。

int a[5] = {10, 20, 30, 40, 50};
int *p = a;
p[2] = 99;              // 等价 *(p+2) = 99,也即 a[2] = 99

2.3 指针相减:得到元素个数

两个指向同一数组的指针相减,结果是它们之间相隔的元素个数(类型 ptrdiff_t,有符号):

int a[5] = {10, 20, 30, 40, 50};
int *p = &a[1], *q = &a[4];
printf("%td\n", q - p);   // 3

注意:指针相减只在同一数组(或同一对象)内才有定义。不同数组的指针相减是未定义行为,别拿它算「两块内存差多远」。

2.4 指针比较:同数组内才有意义

<><=>= 同样只对同一数组内的指针有定义。==/!= 则任意两个指针都能比(判断是否指向同一位置)。

int a[5];
int *p = &a[0], *q = &a[3];
if (p < q) { /* 合法,p 在 q 前面 */ }

遍历数组的两种等价写法:

for (int *p = a; p < a + 5; p++) {   // 指针推进
    printf("%d ", *p);
}
// 完全等价于
for (int i = 0; i < 5; i++) {
    printf("%d ", a[i]);
}

2.5 越界 = 未定义行为

指针运算最容易踩的坑:算出一个越界指针本身(哪怕不解引用)在大多数情况下就是未定义行为。

int a[5];
int *p = a + 5;    // 允许:指向「末尾之后一个」的位置(one-past-the-end)
int *q = a + 6;    // UB:越过 one-past-the-end
int *r = a - 1;    // UB:越过首元素之前
  • 唯一合法的越界指针是 one-past-the-enda + 5),可用来作比较/循环终点,但不能解引用
  • 一旦越界,编译器可能做任何事:读到脏数据(最坑,看着正常),或按「这不会发生」的假设优化出反直觉结果。

2.6 void* 与算术

标准 C 中 void* 不能做算术(编译器不知道 +1 该前进几字节)。GCC/Clang 作为扩展允许 void* 按 1 字节前进,但可移植代码别这么写:

void *p = malloc(8);
// p + 1;                  // 标准 C 报错
char *c = p;               // 先转成 char*,按字节走
c + 1;                     // 前进 1 字节,合法

2.7 与汇编 / 其他语言对照

  • 汇编p + i 就是 lea rax, [p + i*sizeof]——地址计算是一条指令,这就是为什么 C 数组访问极快、接近裸机。
  • Java:引用不能做任何算术,没有 p+1;数组越界抛 ArrayIndexOutOfBoundsException,是运行时检查,不是 UB。安全,但有每次访问的边界检查开销。
  • C++:指针算术与 C 相同;迭代器(iterator)是「更安全的指针算术」抽象,std::vector 迭代器也支持 it + nit1 - it2,且带越界诊断(调试模式)。
  • Python:切片 a[i:j]list[i] 表面像下标,背后是解释器对对象引用数组的封装,没有「指针算术」这种裸内存概念。

步长 = 指向类型大小;下标是语法糖;越界(除 one-past-the-end)即 UB(第 14 章展开)。