第一部分:十分钟彻底搞懂指针(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):如果有两个指针 pq,写 p = q; 的意思是:q 里的门牌号抄一份给 p。结果就是 pq 指向了同一个房间。

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;     

👩‍🏫 终极释义(为什么要这样写?):

  1. typedef:给复杂的名字取个简短的别名。
  2. LNode(别名1):代表“结点本身”。
    • 用法:当你要定义一个辅助指针,在链表里跳来跳去时,用 LNode *p;
  3. *LinkList(别名2):代表“指向结点的指针”。
    • 用法:当你要定义整条链表的“头指针”(代表整条链表)时,用 LinkList list;
    • 本质LinkList 等价于 LNode *,两者底层一模一样,纯粹是为了让人一眼看出“这是链表头”还是“这是普通结点”。

第三部分:异常处理(判卷老师最爱看的满分细节)

在链表题中,所有的操作(查找、插入、删除)都必须先做安全性检查!不写这几句,机考容易爆 0(段错误段核心),笔试会被扣 2 分。

// 标准的异常拦截语句
if (list == NULL || list->link == NULL || k <= 0) {
    return 0; // 或者 return false; 视题目要求而定
}

🔍 拆解为什么要拦截这三个东西:

  1. list == NULL:防“空指针”。连头指针都没有,或者内存分配彻底失败了。如果不拦截,后面强行写 list->link 会直接导致程序崩溃。
  2. list->link == NULL:防“空链表”。头结点是有的,但后面一个有效数据都没有(是一条光秃秃的空链表)。这种情况下你找啥都找不到,直接拦截。
  3. 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

总结一下

  1. 你在图里看到的 *p,和链表里的 *slow,物理动作是一模一样的,都是“去内存地址里把那个本尊拿出来”。

  2. 因为图里的本尊只是个简单的整数,拿出来就直接用了。

  3. 因为链表里的本尊是个结构体(套房),拿出来之后你还得指定要里面的哪一部分,这就衍生出了 (*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)。