C++ / a working model

32 / 80   ·   C++11   ·   约 8 分钟

指针 Pointer:地址、边界与借用

先记住这句话

指针是有类型的值,可能指向对象、函数、尾后位置,也可能为空或无效。非空不是可解引用的证明;正确使用依赖对象仍存活、类型匹配和访问未越界,裸指针本身也不表达释放责任。

本篇内容
  1. 指针本身也是对象
  2. 指针运算围绕数组定义
  3. 接口要补足指针缺少的信息
  4. 运行示例
  5. 动手练习

指针本身也是对象

int* p = &value 创建了一个指针对象,保存指向 value 的指针值。给 p 重新赋地址改变的是指针,给 *p 赋值改变的是被指向的整数。复制指针只增加一种访问路径,不会复制整数,也不会延长整数的生存期。

nullptr 表达没有目标,可用于可选借用。解引用前检查非空只能排除空值,无法排除对象已经销毁、类型不合适或范围已失效。标准没有要求空指针的对象表示是全零字节,所以应使用初始化或赋值,而不是把指针存储逐字节清零。

指针运算围绕数组定义

同一数组内的指针可移动到元素或尾后位置;尾后指针只用于边界判断,不能读取它指向的元素。p + 1 按元素步进,不是把地址机械增加一字节。非数组对象在这些运算规则中可以视为长度为一的数组。

示例使用半开区间 [first, last) 求和,循环在到达尾后位置前停止。两个指针相减要求属于同一数组范围,且差值能由相应类型表示。不同局部变量即使碰巧相邻,也不能当成数组遍历;布局观察不能替代语言保证。

接口要补足指针缺少的信息

单个指针没有携带元素数量或所有权。需要范围的接口,应同时给出长度,或在 C++20 使用 std::span 把地址和长度放在一起。span 仍是借用,它不负责保存被观察的数据,调用者必须保证底层对象活得足够久。

裸指针适合表达可空的非拥有访问,资源拥有者应使用容器或智能指针。示例中的数组由当前作用域管理,firstlast 都只在数组活着时使用,因此无需也绝不能对它们执行 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 超出了允许形成的数组范围,不应计算它。若需要表示逻辑上更远的位置,应先用整数索引进行范围判断,确认合法后再形成指针。

继续查证

标准草案链接会随工作草案更新;本文版本标记对应示例最低要求,不表示草案中的所有新规则都适用于旧标准。

回到目录