指针:地址、解引用与算术
理解指针作为内存地址的本质,学习取地址和解引用运算符、数组中的指针算术、指针的 const 正确性,以及在现代 C++ 中何时仍然适合使用原始指针。
什么是指针?
指针是一个存储内存地址的变量。C++ 中的每个对象都存在于内存中的某个地址上,指针让你可以间接地引用该对象。指针是 C++ 的基础 ── 它们使动态数据结构、多态和高效的参数传递成为可能。
指针类型使用 * 声明:int* p; 声明了一个指向 int 的指针。指针本身占用内存(在 64 位系统上通常为 8 字节),它的值是一个地址,而不是它所指向的数据。
取地址与解引用
两个基本的指针运算符是 &(取地址)和 *(解引用)。& 运算符获取变量的地址。* 运算符沿着地址访问所指向的对象。
#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 风格数组在传递给函数时会丢失其大小信息。
#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 引用:何时使用哪个
引用和指针都提供间接访问,但它们在重要方面有所不同。引用不能为空、不能重新绑定,并且不需要显式解引用。函数参数优先使用引用;当需要可空性或重新绑定时使用指针。
#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*vsint* const - 在现代 C++ 中,原始指针应该仅作为非拥有的观察者
- 除非需要可空性或重新绑定,否则优先使用引用而非指针
测验 — 检验你的知识
(15 经验值)1. `const int* p` 是什么意思?
2. 当数组被传递给接受指针参数的函数时会发生什么?
3. C++ 中指针算术何时有效?