第5.6章: 指针算术

English

概述

指针算术是指对指针加上或减去一个整数。 这让指针按元素个数(而不是字节数)向前或向后移动。 只有当指针指向同一个 C 风格数组的元素时, 指针算术才有意义。

在本章中,你将学到:

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 —— 都保证:

这与 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);

这些函数返回:

这是为 C 风格数组获取迭代器的首选现代方式。

9. 非法的指针算术

C++ 只允许对指针做非常有限的几种算术运算。 超出这些规则的任何操作,要么是编译期错误,要么是未定义行为。

9.1 允许的操作

9.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 的迭代器。这三种容器都提供 连续迭代器,也就是说它们的迭代器行为与裸指针完全一致。

允许的操作:

禁止的操作:


::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 都连续存储元素,它们的迭代器遵循与裸指针相同的规则。

要点