指针与引用
原理
指针的底层本质
指针是一个存储内存地址的变量。在 64 位系统上,所有指针类型的大小均为 8 字节(48 位虚拟地址空间)。指针的类型决定了解引用时的操作宽度和指针算术的步长。
汇编层面对应(x86-64):
int* p = &x;→leaq -4(%rbp), %rax+movq %rax, -8(%rbp)*p = 42;→movq -8(%rbp), %rax+movl $42, (%rax)*(p + 2)→ 加载 p 的值到 rax,addq $8, %rax(2 × sizeof(int) = 8),然后movl (%rax), ...
CPU 的寻址模式中,寄存器间接寻址就是指针的硬件表达:movl (%rax), %ecx 表示”将 rax 寄存器中存储的地址所指向的 4 字节数据加载到 ecx”。
虚拟地址与 MMU
用户程序中的指针值全部是虚拟地址,不是物理内存的真实地址。CPU 内的 MMU(Memory Management Unit)通过页表将虚拟地址转换为物理地址。x86-64 使用四级页表结构(PML4 → PDPT → PD → PT),每次转换最多需要 4 次内存访问,因此 CPU 用 TLB(Translation Lookaside Buffer)缓存最近的转换结果。
地址 0(nullptr)对应的首虚拟页在操作系统中被标记为不可访问,解引用空指针触发 SIGSEGV 段错误。
引用的底层实现
引用在语法层面是别名,在底层实现上就是自带解引用的 const 指针。以下两段代码生成完全相同的汇编:
int addRef(int& x, int& y) { return x + y; }
int addPtr(int* x, int* y) { return *x + *y; }
// 汇编:movl (%rdi), %eax; addl (%rsi), %eax; ret当引用作为类成员时,会实际占用一个指针大小的空间。但在函数参数中,编译器通过寄存器传递——System V ABI 下 this 和引用参数都通过 rdi 传递,与指针完全相同。
右值引用与移动语义的汇编基础
移动语义的”零拷贝”在汇编层体现为几条 mov 指令(窃取指针 + 源对象置空),而非深拷贝所需的 new + memcpy:
Buffer(Buffer&& other) noexcept
: data(other.data), size(other.size) {
other.data = nullptr; // ← 汇编:mov qword [rsi], 0
other.size = 0;
}
// 共约 4-5 条 mov 指令,无系统调用引用折叠规则
模板推导中可能出现引用的引用,C++ 通过折叠规则处理:
T& &→T&T& &&→T&T&& &→T&T&& &&→T&&
这是 std::move 和 std::forward 实现的基础。auto&& 也是转发引用——当绑定的表达式是左值时推导为左值引用,右值时推导为右值引用。
函数指针与虚函数表指针的区别
| 普通指针 | 函数指针 | vptr | |
|---|---|---|---|
| 谁管理 | 程序员 | 程序员 | 编译器自动 |
| 存储内容 | 对象地址 | 函数入口地址 | vtable 地址 |
| 间接调用开销 | 1 次解引用 | 1 次解引用 + 1 次间接跳转 | 2 次解引用 + 1 次间接跳转 |
| 对象所占字节 | 8(64位) | 8 | 8(在对象偏移 0 处) |
函数指针调用 fp(3,5) 在汇编层是 jmp *%rax。虚函数调用 ptr->func() 则需要:先读 vptr → 从 vtable 取函数地址 → 调用,共两次内存读 + 一次间接跳转。
成员函数指针的内部结构
非虚成员函数指针就是函数地址。虚函数指针则需要额外存储对 vtable 的偏移索引。在有虚继承的情况下,成员指针可能达到 16-32 字节,需存储对虚基类的偏移信息。
语法
指针声明与 const 组合
int* p; // 指向 int 的指针
const int* p1; // 指向 const int 的指针(不能通过 p1 修改目标)
int const* p2; // 同上
int* const p3 = &x; // const 指针(p3 本身不能改)
const int* const p4; // 两者都是 const
// 记忆方法:从右向左读指针运算
int arr[5] = {10,20,30,40,50};
int* p = arr; // p 指向 arr[0]
p++; // 偏移 sizeof(int) 字节
ptrdiff_t diff = &arr[4] - &arr[2]; // = 2(元素个数,不是字节数)
// p[n] 等价于 *(p+n),2[p] 等价于 *(2+p)(可编译但不要写)函数指针
using BinaryOp = int (*)(int, int); // C++11 推荐写法
int add(int a, int b) { return a + b; }
BinaryOp op = add; // &add 也可以
int result = op(3, 5); // 隐式解引用调用左值引用与右值引用
int x = 10;
int& lref = x; // 左值引用:绑定到左值
int&& rref = 42; // 右值引用:绑定到右值,延长其生命周期
int&& rref2 = std::move(x); // std::move 将左值转为右值
const int& cref = 42; // const 左值引用可以绑定到右值智能指针(核心用法)
// unique_ptr:独占所有权,sizeof == sizeof(T*)(默认删除器时)
auto up = std::make_unique<Resource>(1);
auto up2 = std::move(up); // 转移所有权,up 变为 nullptr
// shared_ptr:共享所有权,引用计数(sizeof ≈ 2 × sizeof(T*))
auto sp = std::make_shared<Resource>(2);
auto sp2 = sp; // 引用计数 +1
// weak_ptr:不增加引用计数的观察者,用于打破循环引用
std::weak_ptr<Resource> wp = sp;
if (auto locked = wp.lock()) { locked->use(); } // 提升为 shared_ptr指针类型转换
static_cast<Derived*>(basePtr); // 编译期向下转换(不检查)
dynamic_cast<Derived*>(basePtr); // 运行时安全检查(需 RTTI)
const_cast<int*>(&constVal); // 移除 const(危险)
reinterpret_cast<char*>(&intVal); // 二进制位重新解释(极危险)实践
力扣题目:力扣数学题(函数指针),力扣排序(结构体指针排序),力扣排序题(理解指针与数组退化的关系)。
AI 自检:给 AI 以下代码,问”ptr 和 ref 在汇编层面有什么区别?为什么 sizeof(Holder) 是 8 而不是空结构体的 1”?
struct Holder { int& ref; Holder(int& r) : ref(r) {} };建议先阅读:04_动态内存 — 指针管理堆内存的完整机制;05_面向对象(一)类与对象基础 — this 指针和 vptr 的详解。
C 语言关联:C 语言中只存在指针,引用是 C++ 独有的类型安全封装。C 风格二级指针 (char** argv) 和 void* 泛型指针在 C++ 中仍有使用场景但建议优先使用 std::span 或模板。详见 C 指针深度剖析。