跳至内容

指针:地址、解引用与算术

理解指针作为内存地址的本质,学习取地址和解引用运算符、数组中的指针算术、指针的 const 正确性,以及在现代 C++ 中何时仍然适合使用原始指针。

什么是指针?

指针是一个存储内存地址的变量。C++ 中的每个对象都存在于内存中的某个地址上,指针让你可以间接地引用该对象。指针是 C++ 的基础 ── 它们使动态数据结构、多态和高效的参数传递成为可能。

指针类型使用 * 声明:int* p; 声明了一个指向 int 的指针。指针本身占用内存(在 64 位系统上通常为 8 字节),它的值是一个地址,而不是它所指向的数据。

取地址与解引用

两个基本的指针运算符是 &(取地址)和 *(解引用)。& 运算符获取变量的地址。* 运算符沿着地址访问所指向的对象。

address_deref.cpp
#include <iostream>

int main() {
    int value = 42;
    int* ptr = &value;  // ptr holds the address of value

    std::cout << "value:  " << value << '\n';        // 42
    std::cout << "&value: " << &value << '\n';       // e.g. 0x7ffd5a3c
    std::cout << "ptr:    " << ptr << '\n';           // same address
    std::cout << "*ptr:   " << *ptr << '\n';          // 42 (dereference)

    *ptr = 100;  // modify value through the pointer
    std::cout << "value after *ptr = 100: " << value << '\n'; // 100

    int* null_ptr = nullptr;  // points to nothing — safe sentinel
    // *null_ptr = 5;  // UNDEFINED BEHAVIOR — crash on most systems
}

指针算术与数组退化

指针算术仅在数组内部(或末尾之后一个位置)有效。编译器按 sizeof(T) 对算术进行缩放。数组名在大多数上下文中会退化为指向其第一个元素的指针 ── 这就是为什么 C 风格数组在传递给函数时会丢失其大小信息。

pointer_arithmetic.cpp
#include <iostream>
#include <cstddef>  // std::ptrdiff_t

int main() {
    int arr[5] = {10, 20, 30, 40, 50};
    int* p = arr;  // array decays to pointer to first element

    std::cout << *p << '\n';        // 10
    std::cout << *(p + 2) << '\n';  // 30  (moves 2*sizeof(int) bytes)
    std::cout << p[3] << '\n';      // 40  (p[i] is *(p + i))

    // Iterating via pointer
    for (int* it = arr; it != arr + 5; ++it) {
        std::cout << *it << ' ';
    }
    std::cout << '\n';  // 10 20 30 40 50

    // Pointer difference
    std::ptrdiff_t diff = (arr + 4) - arr;  // 4 (elements, not bytes)
    std::cout << "diff: " << diff << '\n';

    // WARNING: arithmetic outside the array is UNDEFINED BEHAVIOR
    // int* bad = arr + 10;  // UB — don't do this
}

const 指针 vs 指向 const 的指针

const 相对于 * 的位置决定了什么是不可变的:

- const int* p(或 int const* p)── 指向 const 的指针:你不能通过 p 修改所指向的值,但你可以将 p 重新指向别处。
- int* const p ── const 指针:指针本身是不可变的(不能重新指向),但你可以修改它所指向的值。
- const int* const p ── 指针和所指向的值都是不可变的。

从右向左读声明:int* const p 读作「p 是一个指向 int 的 const 指针。」

Void 指针(void*)可以持有任何地址,但不能在不进行转换的情况下解引用。它们主要用于 C 风格 API 和分配器实现中。

最佳实践

在现代 C++ 中,原始指针应该专门用作非拥有的、观察者指针。它们表达的意思是:「我可以看到这个对象,但我不对它的生命周期负责。」常见的合法用法:

- 实现数据结构(树节点指向父节点)
- 观察但不拥有的函数参数(不过引用通常更好)
- 与 C API 交互
- 自定义容器中的迭代器

如果指针拥有资源,请将其包装在 std::unique_ptr 或 std::shared_ptr 中。如果你发现自己在写 delete,那在现代 C++ 中你几乎肯定做错了。

常见陷阱

悬空指针是排名第一的指针错误。当指针所指向的对象被销毁时,指针就变成悬空的:

- 返回指向局部变量的指针
- 删除一个对象而另一个指针仍然引用它
- 指向 std::vector 中的元素,而该 vector 在 push_back 后被重新分配

未初始化的指针包含垃圾地址。如果你还没有目标,请始终初始化为 nullptr。编译器不会对所有未初始化指针的使用发出警告。

指针 vs 引用:何时使用哪个

引用和指针都提供间接访问,但它们在重要方面有所不同。引用不能为空、不能重新绑定,并且不需要显式解引用。函数参数优先使用引用;当需要可空性或重新绑定时使用指针。

ptr_vs_ref.cpp
#include <iostream>

void increment_ref(int& val) { ++val; }         // cannot be null
void increment_ptr(int* val) { if (val) ++(*val); } // must null-check

int main() {
    int x = 10;
    increment_ref(x);        // clean call syntax
    increment_ptr(&x);       // caller must take address
    std::cout << x << '\n';  // 12

    int* p = &x;
    p = nullptr;  // OK — pointers can be re-seated and nulled

    int& r = x;
    // r = ???;    // references cannot be re-seated
    // int& bad;   // ERROR — references must be initialized
}
关键要点
  • & 获取地址,* 沿着地址访问 ── 这是一对互逆操作
  • 指针算术仅在数组内部有效,并按 sizeof(T) 进行缩放
  • 数组名退化为指针,丢失大小信息
  • 从右向左读 const 的位置:const int* vs int* const
  • 在现代 C++ 中,原始指针应该仅作为非拥有的观察者
  • 除非需要可空性或重新绑定,否则优先使用引用而非指针

测验 — 检验你的知识

(15 经验值)

1. `const int* p` 是什么意思?

2. 当数组被传递给接受指针参数的函数时会发生什么?

3. C++ 中指针算术何时有效?