指针与引用

原理

指针的底层本质

指针是一个存储内存地址的变量。在 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 字节,需存储对虚基类的偏移信息。

悬空指针的常见模式

悬空指针(dangling pointer)是指向已释放或已失效内存的指针,是 C++ 中最危险的未定义行为来源之一。

// 模式1:返回局部变量地址
int* bad1() {
    int local = 42;
    return &local; // local 在函数返回后销毁,返回的指针悬空
}
 
// 模式2:指向已释放的堆内存
int* bad2() {
    int* p = new int(10);
    delete p;
    return p; // p 指向已释放内存,使用它是 UB
}
 
// 模式3:容器扩容导致迭代器/指针失效
std::vector<int> v = {1,2,3,4,5};
int* ptr = &v[2]; // ptr 指向 v[2]
v.push_back(6); // 可能触发重新分配,ptr 悬空!
 
// 模式4:引用绑定到临时对象
const int& bad4 = 42; // OK,生命周期延长到引用的生命周期
int&& bad5 = 42; // OK,同上
// 但以下不行:
// int& bad6 = 42; // 编译错误:非 const 左值引用不能绑定到右值

指针失效的典型陷阱

// 陷阱1:迭代器失效
std::vector<int> v = {1,2,3,4,5};
auto it = v.begin();
// v.erase(it); // 插入/删除后迭代器失效
// ++it; // 未定义行为!
 
// 陷阱2:野指针(未初始化的指针)
int* p; // 未初始化,可能指向任意地址
// *p = 10; // 未定义行为
 
// 陷阱3:双重释放
int* p = new int(10);
delete p;
delete p; // UB:double free,可能崩溃或数据损坏
 
// 陷阱4:在悬挂的引用上操作
std::string& ref = someFunc(); // 返回局部 string 的引用
// someFunc() 返回后,ref 绑定的临时对象已销毁
// 使用 ref 是未定义行为
 
// 安全做法:使用智能指针或 RAII
auto safe = std::make_unique<int>(42); // 自动管理生命周期
std::shared_ptr<int> shared = std::make_shared<int>(42); // 引用计数

智能指针的性能开销

// unique_ptr:零额外开销(默认删除器时)
// sizeof(unique_ptr<T>) == sizeof(T*)
// 没有引用计数,没有原子操作,编译器可以完全内联
 
// shared_ptr 的代价:
// 1. 额外内存:控制块(引用计数 + 弱引用计数 + 删除器 + 分配器)
//    sizeof(shared_ptr<T>) ≈ 16 字节(两个指针:对象 + 控制块)
// 2. 引用计数操作是原子的(线程安全),每次拷贝/销毁都有原子操作开销
// 3. make_shared 的控制块和对象在同一块内存(减少一次 new)
// 4. 自定义删除器会增加控制块大小
 
struct ControlBlock {
    std::atomic<long> strong_count; // 强引用计数
    std::atomic<long> weak_count;   // 弱引用计数
    // + 删除器(如果有)
    // + 分配器(如果有)
};
 
// weak_ptr 的代价:
// 不增加强引用计数,但增加弱引用计数
// lock() 时需要原子检查强引用计数是否 > 0

语法

指针声明与 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); // 二进制位重新解释(极危险)

使用 new 和 delete 的注意事项

// C++17 起推荐使用 make_unique / make_shared,避免直接 new/delete
// 直接 new 的风险:
// 1. 忘记 delete → 内存泄漏
// 2. 异常导致 delete 未执行 → 内存泄漏
// 3. double delete → 未定义行为
 
// RAII 模式:资源获取即初始化
class FileHandle {
    FILE* f;
public:
    explicit FileHandle(const char* name) : f(fopen(name, "r")) {
        if (!f) throw std::runtime_error("open failed");
    }
    ~FileHandle() { if (f) fclose(f); }
    // 禁止拷贝
    FileHandle(const FileHandle&) = delete;
    FileHandle& operator=(const FileHandle&) = delete;
};
 
// 与智能指针结合:
auto file = std::make_unique<FileHandle>("data.txt");
// 离开作用域时自动调用析构函数关闭文件

实践

练习:数学题(函数指针),排序题(结构体指针排序),排序题题(理解指针与数组退化的关系)。

练习

题号题目链接知识点
206反转链表https://www.luogu.com.cn/problem/P1001指针操作、链表节点的 next 指针重新连接
21合并两个有序链表https://www.luogu.com.cn/problem/P1001双指针遍历、哨兵节点技巧
141环形链表https://www.luogu.com.cn/problem/P1001快慢指针(Floyd 判圈算法)、指针有效性判断
142环形链表 IIhttps://www.luogu.com.cn/problem/P1001悬空指针模式、快慢指针数学推导
203移除链表元素https://www.luogu.com.cn/problem/P1001指针失效场景、哨兵节点避免头节点特殊处理
234回文链表https://www.luogu.com.cn/problem/P1001指针反转与链表中点定位
P1023ticketshttps://www.luogu.com.cn/problem/P1023语法练习
206反转链表https://www.luogu.com.cn/problem/P1001迭代法 vs 递归法的指针操作对比
19删除链表的倒数第 N 个结点https://www.luogu.com.cn/problem/P1001双指针间距技巧、指针失效预防
24两两交换链表中的节点https://www.luogu.com.cn/problem/P1001多指针协调、指针悬空陷阱
25K 个一组翻转链表https://www.luogu.com.cn/problem/P1001复杂指针操作、分组边界处理
138复制带随机指针的链表https://www.luogu.com.cn/problem/P1001哈希表映射指针、指针悬空解决方案
236二叉树的最近公共祖先https://www.luogu.com.cn/problem/P1001递归中指针返回值的含义
148排序链表https://www.luogu.com.cn/problem/P1001链表指针操作与归并排序结合