32 / 80 · C++11 · 约 8 分钟
指针 Pointer:地址、边界与借用
指针是有类型的值,可能指向对象、函数、尾后位置,也可能为空或无效。非空不是可解引用的证明;正确使用依赖对象仍存活、类型匹配和访问未越界,裸指针本身也不表达释放责任。
指针本身也是对象
int* p = &value 创建了一个指针对象,保存指向 value 的指针值。给 p 重新赋地址改变的是指针,给 *p 赋值改变的是被指向的整数。复制指针只增加一种访问路径,不会复制整数,也不会延长整数的生存期。
nullptr 表达没有目标,可用于可选借用。解引用前检查非空只能排除空值,无法排除对象已经销毁、类型不合适或范围已失效。标准没有要求空指针的对象表示是全零字节,所以应使用初始化或赋值,而不是把指针存储逐字节清零。
指针运算围绕数组定义
同一数组内的指针可移动到元素或尾后位置;尾后指针只用于边界判断,不能读取它指向的元素。p + 1 按元素步进,不是把地址机械增加一字节。非数组对象在这些运算规则中可以视为长度为一的数组。
示例使用半开区间 [first, last) 求和,循环在到达尾后位置前停止。两个指针相减要求属于同一数组范围,且差值能由相应类型表示。不同局部变量即使碰巧相邻,也不能当成数组遍历;布局观察不能替代语言保证。
接口要补足指针缺少的信息
单个指针没有携带元素数量或所有权。需要范围的接口,应同时给出长度,或在 C++20 使用 std::span 把地址和长度放在一起。span 仍是借用,它不负责保存被观察的数据,调用者必须保证底层对象活得足够久。
裸指针适合表达可空的非拥有访问,资源拥有者应使用容器或智能指针。示例中的数组由当前作用域管理,first 和 last 都只在数组活着时使用,因此无需也绝不能对它们执行 delete。识别释放责任比记忆地址数值更重要。
容易答错的地方
- 只检查
p != nullptr不能发现悬空;已释放的指针通常不会自动变成空。 - 形成超出数组尾后一格的指针就可能违反规则,不是只有解引用越界才有问题。
运行一个例子
最低标准 C++11 · 完整程序 · 下载 .cpp
#include <cassert>
int main() {
int values[]{2, 4, 6};
int* first = values;
int* last = values + 3;
int sum = 0;
for (int* p = first; p != last; ++p) {
sum += *p;
}
assert(sum == 12);
assert(last - first == 3);
int* optional = nullptr;
assert(optional == nullptr);
optional = &values[1];
*optional = 9;
assert(values[1] == 9);
}
在本地编译
g++ -std=c++11 -Wall -Wextra -Wpedantic -pthread memory-pointers.cpp -o example && ./example预期结果
预期:正常退出、无输出;所有 assert 通过。
CHECK YOUR UNDERSTANDING
合上答案,试着解释。
对于长度为三的 values,values + 3 可以保存吗?可以读取吗?values + 4 呢?
查看参考答案
values + 3 是合法尾后指针,可以保存,并与遍历指针比较,但不能解引用读取。values + 4 超出了允许形成的数组范围,不应计算它。若需要表示逻辑上更远的位置,应先用整数索引进行范围判断,确认合法后再形成指针。
继续查证
标准草案链接会随工作草案更新;本文版本标记对应示例最低要求,不表示草案中的所有新规则都适用于旧标准。