5.2 指针运算与数组名的真相


5.2 指针运算与数组名的真相

本节摘要:指针加减不是整数加减——加一等于"跳一个指向类型宽度"。数组名在大多数场合会退化成首地址,下标访问就是指针运算的语法糖;两个例外(sizeof 与 &)恰恰暴露了数组与指针的真实区别。本节手推地址验证每条规则,并给出指针相减与越界的边界判据。

步长:类型宽度决定跳跃距离

#include <stdio.h> int main(void) { int arr[4] = {10, 20, 30, 40}; int *p = arr; /* 数组名退化成首元素地址 */ printf("p = %p 值 %d\n", (void *)p, *p); printf("p + 1 = %p 值 %d\n", (void *)(p + 1), *(p + 1)); char *c = (char *)p; printf("char视角 +1 = %p\n", (void *)(c + 1)); return 0; }

典型输出:

p = 0x7ffc0a41b440 值 10 p + 1 = 0x7ffc0a41b444 值 20 char视角 +1 = 0x7ffc0a41b441

p + 1 地址涨了 4(一个 int 的宽度),char 视角只涨 1。指针加一的实际含义是"指向下一个同类型元素"。汇编层面:mov eax, [rdi + rcx*4] ——下标乘以元素宽度写死在指令的缩放因子里,步长是编译器按类型算好嵌入的。

合法的指针运算范围很窄:加/减整数、指针减指针(同数组内,结果是元素数)、比较大小(同数组内)。两个无关指针相减或比较是未定义行为;指针加 1 也不总合法——指向数组末元素再 +1 可以当作边界哨兵比较但不能解引用,C 标准为此开了这个专门的口子。

for (int *q = arr; q < arr + 4; q++) /* arr+4 是合法哨兵 */ printf("%d ", *q); /* 循环结束,q == arr + 4,此时 *q 越界 */

数组名:会退化的"地址"

数组与指针是两个类型,但数组名在表达式里几乎总是退化成首元素地址:

int arr[4] = {10, 20, 30, 40}; int *p = arr; /* 等价于 &arr[0] */ p[2] = 99; /* 下标就是指针运算:*(p+2) */ arr[2] = 88; /* 数组写法同样编译成 *(arr+2) */

a[i]*(a + i) 在机器层面完全等价,编译成同一条指令。甚至 2[arr] 都能编译通过(*(2 + arr))——别这么写,但它证明了下标只是语法糖。

两个例外暴露数组名的真实身份:

printf("%zu\n", sizeof arr); /* 16:整个数组的字节数,没退化 */ printf("%p\n", (void *)&arr); /* 数组整体地址:数值上等于首地址,但类型是 int(*)[4] */

sizeof 拿到的是整块数组的大小;&arr 加一跳的是 16 字节(整个数组)而非 4。数组作为函数参数时退化无可挽回:

void f(int a[]) /* 等价于 int *a,方括号里的长度被无视 */ { sizeof a; /* 8:这是指针的大小! */ }

这就是数组必须额外传长度的原因——被调函数拿到的只是一个地址,数组规模信息在退化时丢失。

图 1 下标、指针与地址的三角关系

图 1 下标、指针与地址的三角关系

指针相减:求距离,不是求地址差

int *q = &arr[3]; printf("%td\n", q - arr); /* 3:相差的元素个数 */ printf("%td\n", (char *)q - (char *)arr); /* 12:按 char 视角是 12 字节 */

同一对指针,滤镜不同结果不同。ptrdiff_t 是专为此设计的带符号类型。经典应用是"在数组里查找后算下标":

int *find(int *begin, int *end, int key) { while (begin < end) { if (*begin == key) return begin; /* 找到 */ begin++; } return NULL; } /* 调用侧:idx = find(arr, arr + 4, 30) - arr; */

begin/end 这对指针夹出一段区间,正是 C++ 标准库迭代器区间的思想源头——在 C 里它就是指针运算的自然产物。

越界:机器不设防的地界

int arr[4]; arr[4] = 1; /* 末尾后一格:未定义行为 */ *(arr - 1) = 2; /* 开头前一格:同样未定义 */

机器层面,arr[4]arr[3] 生成完全相同形态的指令——地址计算照做、访存照发。CPU 没有"这是不是数组边界"的概念,越界的后果取决于隔壁住着谁:踩到别的变量(数据错乱)、踩到只读页(段错误)、踩到malloc 元数据(堆损坏,崩在很远的地方)。数组越界是 C 世界头号内存事故来源,没有之一。防范武器第 6 章展开。

⚠️ 常见坑:用 sizeof arr / sizeof arr[0] 求长度时把数组先传进了函数。函数里拿到的已是指针,算出来是 2(8 字节指针除以 4 字节 int)。长度必须在退化之前求好并传过去。

💡 关键直觉:数组是"一排编了号的房间",指针是"一张写着房号的纸条"。纸条可以改写、可以做加减法找邻居;房间编号本身动不了。sizeof& 是仅有的两个能区分"一排房间"和"纸条"的场合。

本节要点回顾

  • 指针加一 = 跨一个类型宽度:步长由类型决定,嵌在指令的缩放因子里。
  • 下标是指针运算的语法糖a[i]*(a+i) 编译成同一条指令。
  • 数组名退化的两例外:sizeof 得整块大小、&arr 跳整块距离;函数参数里的数组必退化,长度要另传。
  • 同数组内才能相减与比较,结果是元素数;末尾哨兵可比较不可解引用。
  • 越界访问的指令与正常访问无异,后果取决于踩到谁——机器不设防,纪律靠自己。

下一节把指针带进函数:传址、输出参数、回调与函数指针。

常见疑问

问:数组名能做加减运算吗? 数组名本身不可赋值、不可自增(它是地址常量),但可以参与表达式运算——arr + 1 合法,arr++ 非法。想移动游标,先复制给一个指针变量。

问:负下标合法吗? 形式上合法(p[-1] 就是指针前移一个元素),前提是仍落在数组范围内。以 arr 为基准写负下标就越界了;以指向数组中部的指针为基准,负下标是常见技巧。

问:多维数组的指针运算怎么算? 按维度逐级退化管理:二维数组名加一跳一整行,取某行元素地址后加一跳一个元素。维度越深,步长层级越多,画地址公式最不容易错。


作者与出处
原作者: 灏天文库
来源:灏天文库
整理: 灏天文库整理
由灏天文库平台收录,内容或由平台用户上传,仅供学习交流
发布者: 作者: 灏天文库 转发
评论区 (0)
U