第5.6章: 指针算术
概述
指针算术是指对指针加上或减去一个整数。 这让指针按元素个数(而不是字节数)向前或向后移动。 只有当指针指向同一个 C 风格数组的元素时, 指针算术才有意义。
在本章中,你将学到:
- 指针算术如何工作
- 为什么指针算术按元素而非字节移动
- 指针相减如何工作
- 什么是
::std::ptrdiff_t - 为什么指针算术只在同一个数组内有效
- 为什么越界算术是未定义行为
- 为什么未定义行为并不保证会崩溃
- 指针就是 C 风格数组的迭代器
::std::ranges::begin(a)与::std::ranges::end(a)如何工作- 哪些指针算术是不允许的
- 为什么同样的规则也适用于
::fast_io::array、::fast_io::vector和::fast_io::string的连续迭代器
1. 指针算术按元素而非字节移动
当给指针加 1 时,它移动到该类型的下一个元素。
int a[3]{10, 20, 30};
int *p = ::std::addressof(a[0]);
int *q = p + 1; // moves to a[1]
Example addresses:
a[0] at 1000
a[1] at 1004
a[2] at 1008
p = 1000
p + 1 = 1004 ← moved by sizeof(int)
指针算术会自动乘以 sizeof(T)。
2. 指针相减
两个指向同一数组元素的指针相减,得到它们之间的 元素个数。
int a[3]{10, 20, 30};
int *p = ::std::addressof(a[0]);
int *q = ::std::addressof(a[2]);
::std::ptrdiff_t diff = q - p; // diff == 2
结果以元素为单位,而不是字节。
2.1 ::std::ptrdiff_t —— 指针差的类型
当你把两个指针相减时,结果存储在一个名为
::std::ptrdiff_t 的类型中。这是 C++
标准专门为表示两个指针之间距离而定义的有符号整数类型。
int a[3]{10, 20, 30};
int *p = ::std::addressof(a[0]);
int *q = ::std::addressof(a[2]);
::std::ptrdiff_t diff = q - p; // diff == 2
::std::ptrdiff_t 始终是有符号类型,因为
指针差可以为负。
fast_io 的保证
fast_io 中所有连续容器 —— 包括
::fast_io::vector、::fast_io::array 和
::fast_io::string —— 都保证:
- 它们的迭代器差类型是
::std::ptrdiff_t - 它们的
.size()返回::std::size_t
这与 C++ 标准库一致,可避免有符号/无符号不匹配带来的隐蔽缺陷。
3. 指针算术只在同一数组内有效
指针算术只在指针指向以下位置时有效:
- 数组的某个元素,或
- 最后一个元素之后的一个位置
其他任何情况都是未定义行为。
int x{5};
int *p = ::std::addressof(x);
// p + 1; // undefined behavior — not part of an array
4. 尾后指针
C++ 允许指针指向数组最后一个元素之后的一个位置。 但你绝不可以解引用它。
int a[3]{10, 20, 30};
int *first = ::std::addressof(a[0]);
int *end = first + 3; // one past the end (valid)
// *end; // undefined behavior — cannot dereference
这个指针存在的唯一目的,是让你能比较它或向后移动。
5. 解引用越界指针是未定义行为
产生越界指针的指针算术是未定义行为。 解引用这样的指针同样是未定义行为。
int a[3]{10, 20, 30};
int *p = ::std::addressof(a[0]);
int *bad = p + 10; // undefined behavior — out of bounds
// *bad; // undefined behavior
未定义行为意味着:
- 程序可能崩溃
- 也可能破坏内存
- 也可能悄悄产生错误结果
- 也可能看起来一切正常
你不能依赖任何特定结果。
WebAssembly 说明
在未启用内存标记的 WebAssembly 上,越界的指针算术通常 不会触发陷阱,使缺陷更难被发现。
6. 未定义行为并不保证会崩溃
许多初学者以为非法的指针算术会立刻崩溃。 这是错的。
编译器可能假定:
- 指针总是指向有效对象
- 指针算术始终保持在数组边界之内
这可能导致编译器以令调试极其困难的方式移除检查或重排代码。
未定义行为之所以危险,恰恰因为它不可预测。
7. 指针就是 C 风格数组的迭代器
在现代 C++ 中,指针被视为最高层次的迭代器。 它们满足所有迭代器要求,并被用作 C 风格数组的迭代器。
对数组 a 而言,迭代器就是:
int a[3]{10, 20, 30};
int *first = ::std::addressof(a[0]);
int *last = first + 3; // one past the end
这些指针的行为与 C++ ranges 库中的迭代器完全一致。
8. 使用 ::std::ranges::begin(a) 与 ::std::ranges::end(a)
现代 C++ 提供了一种统一的方式获取数组的迭代器:
#include <ranges>
int a[3]{10, 20, 30};
int *first = ::std::ranges::begin(a);
int *last = ::std::ranges::end(a);
这些函数返回:
::std::addressof(a[0])(对begin(a))::std::addressof(a[0]) + 3(对end(a))
这是为 C 风格数组获取迭代器的首选现代方式。
9. 非法的指针算术
C++ 只允许对指针做非常有限的几种算术运算。 超出这些规则的任何操作,要么是编译期错误,要么是未定义行为。
9.1 允许的操作
ptr + nptr - n++ptr、--ptrptr += n、ptr -= nptr2 - ptr1(以元素计的距离)
9.2 禁止的操作
ptr + ptrptr * nptr / nptr % nptr1 * ptr2ptr1 / ptr2(ptr1 + ptr2) / 2
int a[3]{10, 20, 30};
int *p = a;
int *q = a + 2;
// p + q; // ❌ illegal
// p * 2; // ❌ illegal
// p / 2; // ❌ illegal
// (p + q) / 2; // ❌ illegal
int *mid = p + (q - p) / 2; // ✔ correct
这能成立,是因为 q - p 是元素个数,而不是指针。
10. 迭代器算术遵循同样的规则
对指针算术的这些限制同样适用于
::fast_io::array、::fast_io::vector 和
::fast_io::string 的迭代器。这三种容器都提供
连续迭代器,也就是说它们的迭代器行为与裸指针完全一致。
允许的操作:
it + nit - n++it、--itit2 - it1
禁止的操作:
it + itit * nit / n(it1 + it2) / 2
::fast_io::vector<int> v{10, 20, 30, 40};
auto it1 = v.begin();
auto it2 = v.begin() + 3;
// it1 + it2; // ❌ illegal
// it1 * 2; // ❌ illegal
// (it1 + it2) / 2; // ❌ illegal
auto mid = it1 + (it2 - it1) / 2; // ✔ correct
这能成立,是因为 it2 - it1 是元素个数。
为什么如此
C++ 标准要求连续迭代器在算术运算方面的行为与指针完全一致。
由于 ::fast_io::array、::fast_io::vector 和
::fast_io::string 都连续存储元素,它们的迭代器遵循与裸指针相同的规则。
要点
- 指针算术按元素移动,而不是字节。
- 指针相减的结果是
::std::ptrdiff_t。 - fast_io 容器用
::std::ptrdiff_t表示迭代器差,用::std::size_t表示大小。 - 指针算术只在同一数组内有效。
- 你可以构造尾后指针,但绝不可解引用它。
- 越界的指针算术是未定义行为。
- 未定义行为并不保证会崩溃。
- 指针就是 C 风格数组的迭代器。
- 获取数组迭代器请使用
::std::ranges::begin(a)与::std::ranges::end(a)。 - 像
ptr + ptr、ptr * n、(ptr1 + ptr2) / 2这类操作是非法的。 array、vector和string的连续迭代器遵循与指针相同的规则。