Java 里你天天写 User u = new User(),从来不想 u 底下是什么——JVM 替你管了。C++ 没有这层遮罩:指针就是「内存地址」这个原始概念本身,你既可以拿它做高效的事(链表、零拷贝传参、和判题沙箱里的系统调用交互),也可能用它造出段错误。理解指针引用不是怀旧,是看懂一切 C++ 代码(包括 STL 内部实现)的前提。
对 Java 背景的你最反直觉的一点:Java 的「引用」更接近 C++ 的指针(可重指向、可为 null),而 C++ 的引用更像「对象的别名」,一旦绑定不可更改、永不独立存在。把这两个词的语义对齐,后面所有课程都顺。
先建立图像
内存就像一排带门牌号的储物柜:
地址 内容
0x1000 int score = 87 ← 变量 score 住在 0x1000
0x2000 0x1000 ← 指针 p 住在 0x2000,里面存的是 0x1000
int* p = &score;
↑ ↑
类型 取地址运算符:拿到 score 的门牌号
"指向 int 的指针"
*p ← 解引用:顺着门牌号找到柜子里的东西(87)
score 的值: 87
score 的地址: 0x7ffd8c2a
p 存的地址: 0x7ffd8c2a
p 指向的值: 87
改后 score: 95
步骤 2:空指针与安全检查
指针可以「不指向任何东西」:
#include <iostream>
int* findScore(int* scores, int n, int target) {
for (int i = 0; i < n; ++i) {
if (scores[i] == target) return &scores[i];
}
return nullptr; // 没找到,返回空指针。C++11 前写 NULL 或 0
}
int main() {
int scores[] = {60, 87, 95};
int* hit = findScore(scores, 3, 100);
if (hit != nullptr) { // 也可以写 if (hit)
std::cout << "找到,值为 " << *hit << "\n";
} else {
std::cout << "未找到\n";
}
// 若不判空直接 *hit,就是对空地址解引用 → 段错误(Segmentation Fault)
// 判题沙箱里用户程序崩溃,最常见的死法就是解引用空指针/野指针
return 0;
}
步骤 3:引用——对象的别名
#include <iostream>
int main() {
int score = 87;
int& r = score; // r 与 score 是同一块内存的两个名字
r = 100; // 改 r 就是改 score
std::cout << score << "\n"; // 100
std::cout << &r << " == " << &score << "\n"; // 地址相同!
// 引用三条铁律:
// 1. 声明时必须初始化(没有「空引用」)
// 2. 绑定后不能改指别的对象(r = x 是赋值给 score,不是重新绑定)
// 3. 永远不会为 null
return 0;
}
步骤 4:传参实战——swap 的三种写法
#include <iostream>
void swapBroken(int a, int b) { // 值传递:复制副本,改不动外面
int t = a; a = b; b = t;
}
void swapByPointer(int* a, int* b) { // 指针传递:传地址,C 风格
int t = *a; *a = *b; *b = t;
}
void swapByRef(int& a, int& b) { // 引用传递:C++ 首选
int t = a; a = b; b = t;
}
int main() {
int x = 1, y = 2;
swapBroken(x, y);
std::cout << x << " " << y << "\n"; // 还是 1 2
swapByRef(x, y);
std::cout << x << " " << y << "\n"; // 2 1
swapByPointer(&x, &y);
std::cout << x << " " << y << "\n"; // 1 2 → 又换回来了
return 0;
}
调用体验上引用完胜:swapByRef(x, y) 不用取地址,语法干净;但指针能表达「可能没有对象」(nullptr),引用不能。参数一定有对象 → 引用;可能缺席 → 指针(现代 C++ 更多用 std::optional,见现代特性课)。
步骤 5:指针的经典战场——链表节点
判题沙箱维护「等待评测的提交队列」这类结构时,链表思想处处可见:
#include <iostream>
struct Submission { // struct:默认 public 的类,先当「数据包」用
int id;
Submission* next; // 指向下一个节点
Submission(int i) : id(i), next(nullptr) {}
};
int main() {
Submission a(101), b(102), c(103);
a.next = &b; // 手工串成链表:a → b → c
b.next = &c;
// 遍历:p 走到 nullptr 为止
for (Submission* p = &a; p != nullptr; p = p->next) {
std::cout << "提交 " << p->id << "\n";
}
// p->next 是 (*p).next 的语法糖:解引用再取成员
return 0;
}
注意这里节点都在栈上、指针只做「连线」,不涉及堆内存分配——所以没有泄漏风险。一旦 new 出堆对象,内存管理规则就变了,那是第 5 课的主题。