第一部分:十分钟彻底搞懂指针(p, *p, &)
在C语言中,记住一个核心口诀:p 是地址(门牌号),*p 是数据(房间里的人),& 是查地址(查房号)。
假设我们有一行测试代码:
int a = 10;
int *p = &a;
1. &:取地址符(查房号)
- 作用:获取一个变量在内存中的真实地址。
- 解释:
&a的意思就是“查一下变量a住在内存的哪个地址”。假设a住在0x100,那么&a的结果就是0x100。
2. p:指针变量本身(装门牌号的盒子)
- 存的是什么:
p里面存的永远是别人的内存地址! - 解释:在上面的代码中,
p里面存的就是0x100。 - 指针的赋值 (
p = q):如果有两个指针p和q,写p = q;的意思是:把q里的门牌号抄一份给p。结果就是p和q指向了同一个房间。
3. *p:解引用(拿着钥匙开门拿东西)
- 存的是什么:
*p代表的是p所指向的那个地址里,真正存放的数据。 - 解释:
p存的是0x100,那么*p就是去0x100这个房间里把人揪出来,也就是10。 - (注意:在定义
int *p时,那个*只是个身份标识,表示 p 是个指针;而在平时写代码用*p时,它才是真正的“开门取数据”动作。)
第二部分:408标准链表结构体定义(默写级)
这段代码在408链表大题中,不管题目有没有给,上来直接默写,白拿规范分!
typedef struct LNode {
int data; // 数据域:真正存放有效数据的地方(比如存 10, 20...)
struct LNode *link; // 指针域:存放下一个结点的内存地址(408常叫 link,平时常叫 next)
} LNode, *LinkList; 👩🏫 终极释义(为什么要这样写?):
typedef:给复杂的名字取个简短的别名。LNode(别名1):代表“结点本身”。- 用法:当你要定义一个辅助指针,在链表里跳来跳去时,用
LNode *p;。
- 用法:当你要定义一个辅助指针,在链表里跳来跳去时,用
*LinkList(别名2):代表“指向结点的指针”。- 用法:当你要定义整条链表的“头指针”(代表整条链表)时,用
LinkList list;。 - 本质:
LinkList等价于LNode *,两者底层一模一样,纯粹是为了让人一眼看出“这是链表头”还是“这是普通结点”。
- 用法:当你要定义整条链表的“头指针”(代表整条链表)时,用
第三部分:异常处理(判卷老师最爱看的满分细节)
在链表题中,所有的操作(查找、插入、删除)都必须先做安全性检查!不写这几句,机考容易爆 0(段错误段核心),笔试会被扣 2 分。
// 标准的异常拦截语句
if (list == NULL || list->link == NULL || k <= 0) {
return 0; // 或者 return false; 视题目要求而定
}🔍 拆解为什么要拦截这三个东西:
list == NULL:防“空指针”。连头指针都没有,或者内存分配彻底失败了。如果不拦截,后面强行写list->link会直接导致程序崩溃。list->link == NULL:防“空链表”。头结点是有的,但后面一个有效数据都没有(是一条光秃秃的空链表)。这种情况下你找啥都找不到,直接拦截。k <= 0:防“找茬数据”。比如题目要求找倒数第 k 个,如果给个k = -5或者k = 0,毫无逻辑意义,直接打回。
考场策略:如果你在考场上一紧张,不确定要不要拦截这么多,至少一定要写上
if (list == NULL) return 0;和if (k <= 0) return 0;,这是底线!
第四部分:🚀 408算法大题/复试机考 答题通用模板
无论考什么链表题(找公共结点、链表逆置、快慢指针),直接把这个壳子套上去!
// ================= 第一步:默写结构体 =================
typedef struct LNode {
int data;
struct LNode *link;
} LNode, *LinkList;
// ================= 第二步:编写核心函数 =================
int Solution_Function(LinkList list, int k) {
// 1. 变量集中声明(为了兼容所有老版本编译器,杜绝报错)
LNode *p;
LNode *q;
// int i; // 如果用到 for 循环,也在这里声明
// 2. 究极异常拦截(展现你严谨的代码素养)
if (list == NULL || list->link == NULL || k <= 0) {
return 0;
}
// 3. 指针初始化(跨过空壳头结点,直奔第一个真实数据)
p = list->link;
q = list->link;
// 4. ========================================
// 这里写题目要求的核心业务逻辑 (while / for)
// 注意:指针往后走永远是 p = p->link;
// ========================================
// 5. 返回结果 (0代表失败,1代表成功,或者返回结点指针)
return 1;
}
// ⚠️ 考研笔试卷面上,写到这里就结束了!千万别写 main 函数!
//或者直接初始化加变量声明
LNode *fast = list->link; // fast 指针指向第一个数据节点
LNode *slow = list->link; // slow 指针指向第一个数据节点
//定义一个 LNode 结构体类型的指针 fast。去查看 list 头结点里面的 link 盒子里装着什么地址,把这个地址拿出来赋值给 fast。这样一来,fast 就直接指向了第一个真实的数据结点。
slow->data 到底是什么?
这就是 C 语言为了让你少写代码发明的“语法糖”。
既然 *slow 是整个节点本尊,那我想访问本尊里面的 data 数据,标准的、最严谨的写法其实是:
C
(*slow).data
意思是:先用 *slow 把节点本尊拽出来,然后用点号 . 访问它里面的 data 成员。(注意括号绝对不能省,因为 . 的优先级比 * 高)。
但是 C 语言的发明者觉得天天写 (*p).xxx 括号太多,太反人类了。于是规定:只要你是一个指针,就可以直接用一个箭头 -> 刺穿进去,拿到里面的成员。
所以: slow->data 完全等价于 (*slow).data
假设节点存的数字是 99,它在内存的地址是 0x1000:
| 代码写法 | 物理意义 | 它到底是什么? |
|---|---|---|
slow | 拿着门牌号的纸条 | 地址 (值为 0x1000) |
*slow | 顺着门牌号找到的整个房子 | 整个结构体 (包含数据 99 和下一根绳子) |
slow->data | 房子里的具体某件物品 | 具体数据 (值为 99) |
核心区别不在于指针,而在于“房间里住的是谁”
图里的口诀非常准确:p 是门牌号,*p 是拿着钥匙开门拿东西。我们来看看当你拿钥匙开门后,拿到的到底是个啥:
场景一:图里的情况(指针指向 int)
C
int a = 10;
int *p = &a;
-
p(门牌号):比如 0x100。 -
开门看(
*p):打开 0x100 这个房间,里面空荡荡的,地板上只躺着一个孤零零的数字 10。 -
结论:因为房间里只有一个东西,所以
*p直接就等于 10。你想打印它,直接printf("%d", *p);完事。
场景二:链表里的情况(指针指向 struct)
C
LNode *slow;
// 假设 slow 已经指向了某个节点
-
slow(门牌号):比如 0x1000。 -
开门看(
*slow):打开 0x1000 这个房间,你发现里面不是一个光秃秃的数字,而是一个大套房(结构体LNode)!这个套房被隔成了两个小单间:-
单间一叫
data(里面放着数据,比如 99)。 -
单间二叫
link(里面放着下一张门牌号)。
-
-
结论:此时的
*slow拿到的是整个大套房。你不能直接printf("%d", *slow);,因为 C 语言不知道你是想打印单间一里的 99,还是想打印单间二里的门牌号。
-> 箭头的诞生:为了在套房里“精准寻物”
既然 *slow 拿到了整个大套房,你必须明确告诉 C 语言你要拿套房里的哪个东西。
最老实、最按部就班的写法: 先开大套房的门(*slow),然后用点号 . 走进指定的单间:
-
拿数据:
(*slow).data -
拿下一张门牌号:
(*slow).link注意:这里必须加括号,因为点号.的优先级比星号*高,不加括号编译器会先算slow.data,直接报错。
懒人发明(也就是链表里的写法): C 语言的发明者觉得天天写 (*slow).xxx 实在太烦人了。既然 slow 是个门牌号,那干脆发明一个“穿墙箭头”吧!
-
拿着门牌号,直接穿墙拿单间一里的东西:
slow->data -
拿着门牌号,直接穿墙拿单间二里的东西:
slow->link
总结一下
-
你在图里看到的
*p,和链表里的*slow,物理动作是一模一样的,都是“去内存地址里把那个本尊拿出来”。 -
因为图里的本尊只是个简单的整数,拿出来就直接用了。
-
因为链表里的本尊是个结构体(套房),拿出来之后你还得指定要里面的哪一部分,这就衍生出了
(*slow).data,也就是简化版的slow->data。
为什么绝对不能写成 slow.data?
你脑子里想的是:“既然 slow 是门牌号,那门牌号里的 data 不就是 slow.data 吗?”
错! 在物理世界里,slow 仅仅是一张写着 0x1000 的小纸条。
一张纸条上会有大套房里的 data 单间吗?绝对不可能。纸条就是纸条,纸条里没有任何数据结构。
在 C 语言的底层语法里,点号 . 极其高贵,它只为“真正的实体(结构体变量本身)”服务。
你对着一个装门牌号的纸条(指针)用点号,编译器会直接骂你:“这是一个指针,你不能在一个指针上找成员!”
怎样才能拿到 data?(点号与箭头的决战)
既然纸条上没有 data,那该怎么办?
步骤一:先用钥匙开门,拿到实体
你得用星号 * 去解引用这个门牌号。
*slow 的动作就是:拿着纸条,跑到 0x1000 这个地址,把里面那座真正的大套房(实体结构体)给拽出来。
步骤二:对实体使用点号 .
现在你手里抓着的是真正的大套房实体了,这时候你终于有资格使用高贵的点号 . 了!
所以,正确的写法是:
(*slow).data
(注意:这里必须加括号把 *slow 包起来,因为在 C 语言里,点号 . 的优先级比星号 * 高。如果不加括号写成 *slow.data,编译器会先算 slow.data,直接报错。)
箭头 -> 到底是个什么神仙发明?
你看看上面那个最严谨的写法 (*slow).data,既要加星号,又要加括号,还要加点号。如果你在考场上手写几百行这种代码,绝对会精神崩溃。
于是,C 语言的发明者为了拯救程序员,硬生生创造了一个专门为指针服务的语法糖——箭头 ->。
当你写下 slow->data 的时候,编译器在底层会瞬间把它翻译成 (*slow).data。
箭头的物理含义就是:“别废话了,你拿着手里这个门牌号(slow),直接给我穿墙过去,把那个房间里的 data 拿出来!”
一句话死记硬背防扣分:
-
只要等号左边是个真正的结构体变量(比如
LNode A;),你就用点号.(如A.data)。 -
只要等号左边是个带星号的指针(比如
LNode *slow;),你就一律用穿墙箭头->(如slow->data)。