笔记1:C语言指针和通用双向循环链表学习记录
概要
本节是笔记1:C语言的指针和通用双向循环链表学习记录。
指针
野指针
- 野指针是指指向不可用内存的指针。当指针被创建时,指针不可能自动指向 NULL,这时默认值是随机的,此时指针成为野指针。
- 当指针被 free 或 delete 释放掉时,如果没有将指针指向 NULL,就会产生野指针,因为释放掉的是指针指向的内存,没有将指针本身释放掉。造成野指针的原因也可能是指针操作超越了变量的作用范围。比如数组越界,避免野指针的方法即使使用完之后释放内存,并将指针赋 NULL。
空指针
- 空指针是指没有指向任何一个存储单元的指针。
通用双向循环链表
通用双向循环链表可以包含单链表、通用链表、双向链表、循环链表的全部知识,所以只记录学习通用双向循环链表即可。
通用性
- 指链表中结点包含数据域和指针域,而通用链表的指针域通常用通用的node指针来表示,与该链表本身类型关系不大,只需要包含通用指针域即可。如:(node为通用指针域)
1 | |
双向性
- 指针域包含前向指针pre和后向指针next
1
2
3
4
5
6//**************指针域************//
//该结构体是专门的指针域,包含在每个节点之中
typedef struct node_t{
struct node_t *pre; //前向指针
struct node_t *next; //后向指针
}NODE,*PNODE;
循环性
- 链表尾部结点的next需要指向链表头,而非NULL,形成一个循环,后续初始化链表会有代码体现。
新建和初始化链表
- 初始化链表就是构造链表头结构体,分配内存。
1 | |
1 | |
可以在main函数中直接新建链表让编译器自动为我们分配内存
1 | |
在这里我们可以发现,在建立循环链表的时候使需要把pre和next属性都指向自己的地址,形成循环。
尾插法插入结点
- 获取链表尾部,此时链表里面只有头结点,所以头结点作为我们的尾部
- 将被添加结点的next属性指向尾部结点的next属性
- 将尾部结点的next属性指向被添加结点地址
- 将被添加结点的pre属性指向尾部结点
- 将此时被添加结点的next属性指向的结点的pre属性指向被添加结点。
1 | |
链表中间插入结点
与尾插法的方法一致,只是这里知道被插入结点是位于哪个结点后面,就不需要获取尾部结点了。
1 | |
删除结点
- 被删除结点左侧结点next属性指向右侧结点
- 被删除结点右侧结点pre属性指向左侧结点
1
2
3
4
5
6
7
8
9
10/********************************************************
* Description : 删除一个节点
* @del_node :被删除的节点指针
* Return :无
**********************************************************/
void DelItemFromList(PNODE del_node)
{
del_node->next->pre = del_node->pre;
del_node->pre->next = del_node->next;
}
==反推数据域==
根据指针域反推数据域是==通用链表==的核心知识,通用链表只通过node指针域进行连接,每个不同类型的链表都有自己的数据域格式,我们需要通过node反推出数据域的地址才能访问到里面的变量
学生链表:
1 | |
1 | |
- 我们来分析这行代码,首先是最终值,它是一个
(struct student *)指针,我们的目的就是获取这样一个指针,使他可以访问到student学生结构体的所有信息,如num、name。 - 我们所拥有的是
node结点的地址,因为这可以从链表中获取到。 - 这行代码中的
p就是我们从链表中获取的node结点地址 - 内存中结构体地址的递增的,所以我们只要获取到
student结构体首地址到node属性的地址处递增了多少字节,然后我们再用p减去递增的字节数就可以得到student的首地址了。 (struct student *)0)->node这行代码是利用0地址下使用struct student *去访问node属性,为什么是0地址,因为我们把这个指针设置成0地址之后此时node属性的地址就正好是student结构体中储存node属性递增的字节数了。(unsigned int)&((struct student *)0)->node可看到中间有个取址符号,此时它的地址就是递增字节数,我们把他强转成(unsigned int)告诉编译器进行普通的指针加减运算。(char *)p这里要强转char *告诉编译器进行普通指针加减运算。(struct student *)最后强转成我们想要的student首地址,接下来就可以通过这个指针访问里边的变量了。
链表排序
使用冒泡排序法,做两层排序,四步走
两个结点,一个比较结点,一个被比较结点。比较结点是外循环,被比较结点内循环
外循环的比较结点固定,然后内循环开始递增,逐个比较。
比较函数后面讲,先关注排序逻辑,我们从小到大排序,比较函数返回小于时不交换,比较函数返回大于时进行交换
==结点交换的实现==:
- 记录下两个结点的前一个结点地址
- 将两个结点都删除
- 结点2插入结点1前一个结点之后,结点1插入结点2前一个结点之后
还需要判断两个结点相邻的情况,相邻情况下需要特殊处理,删除结点之后直接将结点2插入结点1之后即可1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43void SortList(PLIST pList)
{
struct node_t *pre1 = &pList->head; //从链表头开始
struct node_t *pre2; //这个是用来记录上一次比较时被比较数的pre指针
struct node_t *cur = pre1->next; //开始比较的第一个结点
struct node_t *next; //类比数组的索引,冒泡排序的i,j
struct node_t *tmp; //用于交换的中间结点
while (cur != &pList->head) //当前比较的目标不是链表尾部
{
pre2 = cur;
next = cur->next;
while (next != &pList->head) //冒泡排序第二层
{
if (CmpStudentNum(cur, next) == 0)
{
/* 交换节点 */
/* 1. del cur */
DelItemFromList(cur);
/* 2. del next */
DelItemFromList(next);
/* 3. 在pre1之后插入next */
AddItemAfter(pre1, next); //pre1记录的是比较数的pre指针
/* 4. 在pre2之后插入cur */
if (pre2 == cur) //如果被比较数pre指针等于当前节点的指针,说明两个节点相邻
AddItemAfter(next, cur);
else
AddItemAfter(pre2, cur);
/* 5. cur/next指针互换 */
tmp = cur;
cur = next;
next = tmp;
}
pre2 = next; //这个是用来记录上一次比较时被比较数的pre指针
next = next->next; //继续往前比较
}
pre1 = cur; //这个是用来记录上一次比较时比较数的pre指针
cur = cur->next; //继续往前比较
}
}
比较函数
1 | |
宏定义实现前面说的反推数据域操作,实现原理是一样的。
1 | |
代码很简单,反推出数据域之后比较他们的num参数,根据结果进行返回即可。
完整工程代码
完整工程代码如下,已经过验证,可以正常运行。
1 | |
笔记1:C语言指针和通用双向循环链表学习记录
http://example.com/2024/08/30/笔记1:C语言的指针和通用双向循环链表学习记录/