CC++ & Algorithm

指针算术:C++里最优雅的“一步之遥”

2026年9月17日关联知识点:C++指针与数组16 次阅读

先抛一个观点:指针之所以让人又爱又恨,根本原因不在于它“指向”什么,而在于它“走”多远。

很多语言都有引用、有对象句柄、有迭代器,但只有C和C++的指针把“地址算术”这件事摆到了台面上。你写 p + 1,编译器不会傻乎乎地把地址值加1,它会根据 p 指向的类型自动缩放步长。这个设计看起来不起眼,却直接决定了数组遍历、内存布局理解、甚至性能优化的底层逻辑。

今天我们就从这个“缩放步长”切入,把指针与数组的关系彻底捋清楚。

数组名到底是什么?

先看一行代码:

int scores[5] = {80, 90, 75, 88, 92};

scores 是什么?初学者会说“这是一个数组”。没错,但在C++的表达式中,数组名会退化(decay)为首元素的地址。也就是说,scores 在大多数语境下等价于 &scores[0]。

这意味着你可以直接写:

int *ptr = scores;  // ptr 指向第一个元素

不需要加 &。因为 scores 本身就是一个地址值。

但这里有个微妙之处——数组名不是指针变量。它是一个常量地址,你不能对它做自增:

scores++;  // 编译错误:数组名不可修改

而指针变量可以:

int *ptr = scores;
ptr++;     // 合法,ptr 现在指向第二个元素

这个区别看似细小,却是理解后续所有指针算术的基础。

指针加1,到底加了几个字节?

这是指针算术最核心的问题。

假设 int 占4个字节,ptr 指向地址 0x1000。执行 ptr++ 后,ptr 的值不是 0x1001,而是 0x1004。

为什么?因为编译器知道 ptr 是 int* 类型,它会把“1”翻译成“1个 int 的大小”。这就是指针算术的自动缩放。

用生活比喻来说:数组是一排连续的房间,每个房间占4平方米。你手里的门牌号是 0x1000。当你“走到下一个房间”时,你跨过的是4平方米的距离,而不是1平方米。指针算术帮你自动计算了这个跨度——你只需要说“下一个”,不需要说“加4”。

这也解释了为什么不同类型的指针不能随意混用。char* 加1只跳1个字节,double* 加1跳8个字节。类型决定了步长。

一道题看清指针算术的本质

来看这道题:

int arr[5] = {10, 20, 30, 40, 50};
int *p = arr;
p += 2;
// *p 的值是多少?

答案不是10,不是20,是 30。

逐步拆解:

  1. p = arr → p 指向 arr[0],地址假设为 0x1000
  2. p += 2 → 编译器翻译为 p = p + 2 * sizeof(int) → p = 0x1000 + 8 = 0x1008
  3. 0x1008 正是 arr[2] 的地址
  4. *p 取出 arr[2] 的值 → 30

这里的关键认知是:p += 2 修改的是指针本身,而不是它指向的值。移动完成后,*p 才去解引用取值。很多初学者会把“移动”和“取值”混为一谈,写出 *p + 2 这种表达式——那是把 arr[0] 的值10加上2得到12,跟地址移动完全是两码事。

地址运算的两种“面孔”

再看一道题,这道题暴露了一个更隐蔽的坑:

int a[4] = {1, 2, 3, 4};
// 下列哪个表达式与 &a[2] 相等?
// A. a + 2    B. *a + 2    C. a[2]    D. &a + 2

正确答案是 A。

逐个分析为什么:

  • A. a + 2 → a 退化为 &a[0],加2跳过两个 int,正好是 &a[2]。✅
  • B. *a + 2 → *a 是 a[0] 的值,即 1。1 + 2 = 3,这是个整数,不是地址。❌
  • C. a[2] → 这是元素的值 3,不是地址。❌
  • D. &a + 2 → 这里 &a 的类型是 int(*)[4],即“指向长度为4的int数组的指针”。加2意味着跳过两个完整的数组,偏移量是 2 × 4 × sizeof(int) = 32 字节,远远超过 a[2] 的位置。❌

D选项是这道题真正的陷阱。它揭示了一个事实:a 和 &a 的值相同,但类型不同,运算步长也完全不同。

  • a 退化为 int*,步长 = sizeof(int) = 4字节
  • &a 是 int(*)[4],步长 = sizeof(int[4]) = 16字节

同样是加2,前者偏移8字节,后者偏移32字节。值相同,类型不同,行为天差地别。这就是C++类型系统的威力——它让地址运算有了“语义”。

指针遍历数组:为什么 ptr++ 比 i++ 更本质

回到遍历数组的场景:

int *ptr = scores;
for (int i = 0; i < 5; i++) {
    cout << *ptr << endl;
    ptr++;
}

这段代码用 ptr++ 遍历,和用 scores[i] 遍历,最终效果一样。但理解层面不同:

  • scores[i] 是下标抽象——你告诉编译器“我要第i个元素”,编译器帮你算地址
  • ptr++ 是地址抽象——你直接操作地址,每次跨过一个元素

后者更接近机器的真实行为。在底层,scores[i] 本质上就是 *(scores + i)。下标运算符 [] 不过是指针算术的语法糖。

理解了这一点,你就能明白为什么C风格字符串可以用指针遍历,为什么函数传数组实际上传的是首地址,为什么动态内存分配返回的是指针——它们全都建立在同一套地址算术模型上。

越界:指针算术的暗面

指针算术给了你极大的自由,也给了你极大的犯错空间。

int scores[3] = {10, 20, 30};
int *ptr = scores;
for (int i = 0; i <= 3; i++) {  // i<=3 越界!
    cout << *ptr << endl;
    ptr++;
}

当 i=3 时,ptr 已经指向了数组末尾之后的内存。那块内存可能属于其他变量,可能是未映射的页,可能恰好能读——但无论如何,这是未定义行为。C++标准不对越界访问做任何保证,编译器也不会帮你检查。

指针算术不会做边界检查,这是它快的原因,也是它危险的原因。你享受了性能,就要承担正确性的责任。

进阶方向

指针与数组的配合是C++内存模型的入口。掌握它之后,你可以自然地过渡到:

  • 指针与字符串:C风格字符串就是字符数组,char* 遍历每个字符
  • 指针作为函数参数:传数组名就是传首地址,函数内修改会影响原数组
  • 动态内存:new 返回指针,delete 释放指针指向的内存
  • 指针与结构体:用 -> 运算符通过指针访问成员,构建链表、树等数据结构

指针算术不是孤立的语法点,它是理解C++内存布局的一把钥匙。你越早习惯用“地址+步长”的思维看代码,就越能写出高效、可控的程序。


关于作者

我是赵老师,持有 NOI 信息学奥赛教练证书,拥有 15 年以上的软件开发经验,从事信息学少儿编程教学已有 8 年时间。

这些年累计帮助 多名 学生通过编程特长升入自己心仪的目标学校。

如果你在编程学习上有任何疑问,欢迎联系我:18620372957(微信同号)

这篇文章对你有帮助吗?

成为第一个评价的人

评论0

还没有评论,来抢沙发~

评论加载中...

想系统学习这个知识点?查看完整知识点 →