指针与引用

原理

指针的底层本质

指针是一个存储内存地址的变量。在 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::movestd::forward 实现的基础。auto&& 也是转发引用——当绑定的表达式是左值时推导为左值引用,右值时推导为右值引用。

函数指针与虚函数表指针的区别

普通指针函数指针vptr
谁管理程序员程序员编译器自动
存储内容对象地址函数入口地址vtable 地址
间接调用开销1 次解引用1 次解引用 + 1 次间接跳转2 次解引用 + 1 次间接跳转
对象所占字节8(64位)88(在对象偏移 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 以下代码,问”ptrref 在汇编层面有什么区别?为什么 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 指针深度剖析