适用对象:有一定 C++ 基础,正在备赛 CSP-J/S 或 NOIP 的学生。
一句话先记住:
指针存地址,引用是别名。竞赛里引用天天用,指针偶尔见。
一、先搞懂内存地址
变量存在内存里,每个变量都有自己的“门牌号”——地址。
inta=10;cout<<&a;// 输出 a 的地址,比如 0x7ffee3f9a8dc&a就是取 a 的地址。
二、指针(Pointer)
指针就是一个存地址的变量。
inta=10;int*p=&a;// p 存着 a 的地址核心操作
| 代码 | 含义 |
|---|---|
int *p | 定义一个指针变量 |
p = &a | 让 p 指向 a |
*p | 取 p 指向的那个变量(解引用) |
*p = 20 | 通过指针修改 a 的值 |
inta=10;int*p=&a;*p=20;// a 变成 20intb=30;p=&b;// p 改为指向 b(指针可以换指向)*p=40;// b 变成 40指针能换指向,这是它和引用最大的不同。
三、引用(Reference)
引用就是给变量起个外号、别名。
inta=10;int&r=a;// r 是 a 的别名核心操作
| 代码 | 含义 |
|---|---|
int &r = a | r 是 a 的引用,必须初始化 |
r = 20 | 就是a = 20 |
&r | 取 a 的地址(r 没有自己的地址) |
inta=10;int&r=a;r=20;// a 变成 20intb=30;// 引用不能换绑!r = b 只是把 b 的值赋给 a四、指针 vs 引用:核心区别
| 对比维度 | 指针 | 引用 |
|---|---|---|
| 本质 | 存地址的变量 | 变量的别名 |
| 必须初始化? | 否,可以先定义后赋值 | 是,必须绑定到一个变量 |
| 能否为空? | 可以(nullptr) | 不能 |
| 能否改变指向? | 能,可以指向别的变量 | 不能,一生绑定一个 |
| 如何访问值? | 需要*解引用 | 直接使用,像普通变量 |
| 内存占用? | 占用内存(存地址) | 不占额外内存(本质是别名) |
| 安全性 | 容易出错(空指针、野指针) | 安全,不容易出错 |
| 竞赛中用哪个? | 用于底层实现 | 用于日常编码(传参、遍历) |
五、->运算符是什么?
->是竞赛中天天要用的运算符,它的本质是:
p->member 等价于(*p).member就是“取出指针指向的那个东西,然后访问它的成员”。
.和->的区别
| 左边是什么 | 用什么 | 示例 |
|---|---|---|
| 普通对象 | . | Node u; u.val = 10; |
| 指针 | -> | Node *p; p->val = 10; |
| 引用 | . | Node &r = u; r.val = 10; |
structNode{intval;Node*left,*right;};Node u;Node*p=&u;Node&r=u;u.val=10;// 对象 → 用 .p->val=20;// 指针 → 用 ->r.val=30;// 引用 → 用 .(跟对象一样)六、竞赛实战场景
场景一:函数传参(避免拷贝,最常用)
voiddfs(Node*u){if(u==nullptr)return;cout<<u->val;dfs(u->left);dfs(u->right);}场景二:范围遍历修改
for(int&x:vec){x*=2;// 直接改原数组,不拷贝}场景三:手写二叉树/链表
structNode{intval;Node*next;// 指向下一个节点};voidprint(Node*head){for(Node*p=head;p!=nullptr;p=p->next){cout<<p->val<<" ";}}场景四:链式前向星(数组模拟指针)
structEdge{intto,w,next;// next 就是“指针”,存的是下一条边的下标}edges[N];inthead[N];voidadd(intu,intv,intw){edges[++cnt]={v,w,head[u]};head[u]=cnt;// 把新的边的下标存进去}// 遍历:用 . 访问(因为下标不是指针)for(inti=head[u];i!=-1;i=edges[i].next){// 处理 edges[i]}场景五:Node *&—— 指针的引用
voidinsert(Node*&u,intx){if(u==nullptr){u=newNode{x,nullptr,nullptr};return;}if(x<u->val)insert(u->left,x);elseinsert(u->right,x);}Node*root=nullptr;insert(root,10);// root 本身被修改了(从 nullptr 变成指向新节点)为什么用Node *&而不是Node *?
// 如果写成 Node *u(传指针的值)voidinsert(Node*u,intx){u=newNode{x,nullptr,nullptr};// 修改的是指针的副本}// 外面的 root 还是 nullptr,改不了!// 要用引用传递指针本身voidinsert(Node*&u,intx){// u 是 root 的别名u=newNode{x,nullptr,nullptr};// 直接修改 root}一句话:想修改指针本身指向什么,就得传指针的引用。
七、常见陷阱
| 错误 | 正确做法 |
|---|---|
int *p; *p = 10;没初始化就用 | 定义时赋值int *p = &a; |
Node *p = nullptr; cout << p->val;空指针解引用 | 用前检查if (p != nullptr) |
Node *p; cout << p->val;野指针 | 初始化Node *p = new Node; |
int &r;引用没初始化 | 必须初始化int &r = a; |
八、记牢这张图
指针和引用 │ ┌───────────────┴───────────────┐ │ │ 指针 引用 │ │ 存地址的变量 变量的别名 可以重新指向 不能换绑 可以为空 必须初始化 用 * 解引用 直接用 用 -> 访问成员 用 . 访问成员九、对竞赛来说够用的标准
| 必须掌握 | 理解即可 | 暂时不用管 |
|---|---|---|
| 引用作函数参数 | 指针的基本概念 | 二级指针int **p |
范围遍历用&修改 | 指针和引用的区别 | 函数指针 |
理解*和&含义 | nullptr | new/delete |
->访问结构体成员 | Node *&的用法 | 复杂指针运算 |
十、一句话总结
指针是“存地址的变量”,引用是“变量的别名”。
引用安全、直观、高效,竞赛里天天用。指针灵活但容易犯错,主要用于底层实现和高级数据结构。掌握引用的用法,理解指针的概念,够了。
十一、检查你是否掌握了
inta=5,b=10;int*p=&a;int&r=b;*p=20;// a = ?r=30;// b = ?cout<<a<<" "<<b<<endl;// 输出什么?答案:a = 20,b = 30
附录:你可能还疑惑的
Q1:为什么引用的本质是“别名”?
因为引用不占额外内存,它和原变量共用同一块内存地址。对引用做任何操作,都直接反映在原变量上。
Q2:int *&和int &*哪个对?
int *&是对的(指针的引用)。int &*是错的(引用的指针,不存在)。
Q3:竞赛里指针和引用哪个重要?
引用重要得多。函数传参、STL 容器操作都离不开引用。指针主要用于手写链表、二叉树、平衡树等数据结构的底层实现。