概要
本人正在准备24年秋招,现记录从现在开始的复习笔记。
本节是笔记1:C语言的指针和通用双向循环链表学习记录。
指针
野指针
- 野指针是指指向不可用内存的指针。当指针被创建时,指针不可能自动指向 NULL,这时默认值是随机的,此时指针成为野指针。
- 当指针被 free 或 delete 释放掉时,如果没有将指针指向 NULL,就会产生野指针,因为释放掉的是指针指向的内存,没有将指针本身释放掉。造成野指针的原因也可能是指针操作超越了变量的作用范围。比如数组越界,避免野指针的方法即使使用完之后释放内存,并将指针赋 NULL。
空指针
- 空指针是指没有指向任何一个存储单元的指针。
通用双向循环链表
通用双向循环链表可以包含单链表、通用链表、双向链表、循环链表的全部知识,所以只记录学习通用双向循环链表即可。
通用性
- 指链表中结点包含数据域和指针域,而通用链表的指针域通常用通用的node指针来表示,与该链表本身类型关系不大,只需要包含通用指针域即可。如:(node为通用指针域)
//学生结构体
typedef struct student{
int num;
char name[20];
float score;
NODE node; //通用指针域类型的变量
}STU,*PSTU;
双向性
- 指针域包含前向指针pre和后向指针next
//**************指针域************//
//该结构体是专门的指针域,包含在每个节点之中
typedef struct node_t{
struct node_t *pre; //前向指针
struct node_t *next; //后向指针
}NODE,*PNODE;
循环性
- 链表尾部结点的next需要指向链表头,而非NULL,形成一个循环,后续初始化链表会有代码体现。
新建和初始化链表
- 初始化链表就是构造链表头结构体,分配内存。
//******链表********//
/* 构造链表头结构体 */
typedef struct List{
char *name;
NODE head;
}LIST,*PLIST;
PLIST stu_list; //新建链表
可以在main函数中直接新建链表让编译器自动为我们分配内存
/********************************************************
* Description : 新建一个链表头部
* @List :已分配好内存的链表
* Return :无
**********************************************************/
void NewListInit(PLIST List)
{
List->head.next = &List->head; //头部next属性设置为自己
List->head.pre = &List->head; //头部pre属性设置为自己
}
在这里我们可以发现,在建立循环链表的时候使需要把pre和next属性都指向自己的地址,形成循环。
尾插法插入结点
- 获取链表尾部,此时链表里面只有头结点,所以头结点作为我们的尾部
- 将被添加结点的next属性指向尾部结点的next属性
- 将尾部结点的next属性指向被添加结点地址
- 将被添加结点的pre属性指向尾部结点
- 将此时被添加结点的next属性指向的结点的pre属性指向被添加结点。
/********************************************************
* Description : 添加一个节点至链表尾部
* @pList :需要添加节点的链表指针
* @new_node :被添加的节点指针
* Return :无
**********************************************************/
void AddItemToList(PLIST pList, PNODE new_node)
{
PNODE last = pList->head.pre; //找到链表尾部节点
new_node->next = last->next; //设置新添加节点next属性
last->next = new_node; //设置尾部(当前)节点next属性
new_node->pre = last; //设置新添加节点pre属性
pList->head.pre = new_node; //设置新添加节点的next节点pre属性
}
链表中间插入结点
与尾插法的方法一致,只是这里知道被插入结点是位于哪个结点后面,就不需要获取尾部结点了。
/********************************************************
* Description : 插入一个节点至目标节点后面
* @Target :目标节点指针
* @new_node :被添加的节点指针
* Return :无
**********************************************************/
void AddItemAfter(PNODE Target, PNODE new_node)
{
new_node->next = Target->next; //设置新添加节点next属性
Target->next = new_node; //设置目标节点next属性
new_node->next->pre = new_node; //设置新添加节点的next节点pre属性
new_node->pre = Target; //设置新添加节节点pre属性
}
删除结点
- 被删除结点左侧结点next属性指向右侧结点
- 被删除结点右侧结点pre属性指向左侧结点
/********************************************************
* 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反推出数据域的地址才能访问到里面的变量
学生链表:
//学生结构体
typedef struct student{
int num;
char name[20];
float score;
NODE node; //通用指针域类型的变量
}STU,*PSTU;
(struct student *)((char *)p - (unsigned int)&((struct student *)0)->node)
- 我们来分析这行代码,首先是最终值,它是一个
(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之后即可
void 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; //继续往前比较
}
}
- 比较函数
#define container_of(ptr, type, member) \
(type *)((char *)ptr - (unsigned int)&((type *)0)->member)
宏定义实现前面说的反推数据域操作,实现原理是一样的。
/********************************************************
* Description : 比较两个学生的num属性
* @pre :目标节点1
* @next :目标节点2
* Return :大于时返回0, 小于时返回-1
**********************************************************/
int CmpStudentNum(PNODE pre, PNODE next)
{
PSTU p;
PSTU n;
p = container_of(pre, struct student, node);
n = container_of(next, struct student, node);
if (p->num < n->num)
return -1;
else
return 0;
}
代码很简单,反推出数据域之后比较他们的num
参数,根据结果进行返回即可。
完整工程代码
完整工程代码如下,已经过验证,可以正常运行。
#include "stdio.h"
#include "stdlib.h"
#define container_of(ptr, type, member) \
(type *)((char *)ptr - (unsigned int)&((type *)0)->member)
//**************指针域************//
//该结构体是专门的指针域,包含在每个节点之中
typedef struct node_t{
struct node_t *pre; //前向指针
struct node_t *next; //后向指针
}NODE,*PNODE;
//******链表********//
typedef struct List{
char *name;
NODE head;
}LIST,*PLIST;
//学生结构体
typedef struct student{
int num;
char name[20];
float score;
NODE node; //通用指针域类型的变量
}STU,*PSTU;
typedef struct teacher //教师
{
int num;
char name[20];
char subj[20];
NODE node; //同上
}TEA,*PTEA;
/********************************************************
* Description : 新建一个链表头部
* @List :已分配好内存的链表
* Return :无
**********************************************************/
void NewListInit(PLIST List)
{
List->head.next = &List->head; //头部next属性设置为自己
List->head.pre = &List->head; //头部pre属性设置为自己
}
/********************************************************
* Description : 添加一个节点至链表尾部
* @pList :需要添加节点的链表指针
* @new_node :被添加的节点指针
* Return :无
**********************************************************/
void AddItemToList(PLIST pList, PNODE new_node)
{
PNODE last = pList->head.pre; //找到链表尾部节点
new_node->next = last->next; //设置新添加节点next属性
last->next = new_node; //设置尾部(当前)节点next属性
new_node->pre = last; //设置新添加节点pre属性
pList->head.pre = new_node; //设置新添加节点的next节点pre属性
}
/********************************************************
* Description : 插入一个节点至目标节点后面
* @Target :目标节点指针
* @new_node :被添加的节点指针
* Return :无
**********************************************************/
void AddItemAfter(PNODE Target, PNODE new_node)
{
new_node->next = Target->next; //设置新添加节点next属性
Target->next = new_node; //设置目标节点next属性
new_node->next->pre = new_node; //设置新添加节点的next节点pre属性
new_node->pre = Target; //设置新添加节节点pre属性
}
/********************************************************
* Description : 删除一个节点
* @del_node :被删除的节点指针
* Return :无
**********************************************************/
void DelItemFromList(PNODE del_node)
{
del_node->next->pre = del_node->pre;
del_node->pre->next = del_node->next;
}
/********************************************************
* Description : 链表排序
* @pList :目标链表
* Return :无
**********************************************************/
int CmpStudentNum(PNODE pre, PNODE next);
void 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; //继续往前比较
}
}
/********************************************************
* Description : 比较两个学生的num属性
* @pre :目标节点1
* @next :目标节点2
* Return :大于时返回0, 小于时返回-1
**********************************************************/
int CmpStudentNum(PNODE pre, PNODE next)
{
PSTU p;
PSTU n;
p = container_of(pre, struct student, node);
n = container_of(next, struct student, node);
if (p->num < n->num)
return -1;
else
return 0;
}
/********************************************************
* Description : 打印链表
* @pList :目标链表
* Return :无
**********************************************************/
void PrintList(PLIST pList)
{
int i = 0;
PNODE node_1 = pList->head.next; //得到链表头部的next属性
PSTU p;
while (node_1 != &pList->head) //如果头部的next不是头部,代表还没有到链表尾部
{
p = container_of(node_1, struct student, node);
printf("person %d: num is %d\r\n", i++, p->num);
/* 后面还有人, 移动到下一个 */
node_1 = node_1->next;
}
}
int main(int argc, char *argv[])
{
PLIST stu_list;
PSTU stus1,stus2,stus3,stus4,stus5,stus6;
stu_list = (PLIST)malloc(sizeof(LIST)); //分配内存
NewListInit(stu_list); //初始化链表
stus1 = (PSTU)malloc(sizeof(STU)); //分配内存
stus2 = (PSTU)malloc(sizeof(STU)); //分配内存
stus3 = (PSTU)malloc(sizeof(STU)); //分配内存
stus4 = (PSTU)malloc(sizeof(STU)); //分配内存
stus5 = (PSTU)malloc(sizeof(STU)); //分配内存
stus6 = (PSTU)malloc(sizeof(STU)); //分配内存
stus1->num = 20;
stus2->num = 40;
stus3->num = 30;
stus4->num = 10;
stus5->num = 50;
stus6->num = 100;
AddItemToList(stu_list, &stus1->node); //添加进链表尾部,尾插法
AddItemToList(stu_list, &stus2->node); //添加进链表尾部,尾插法
AddItemToList(stu_list, &stus3->node); //添加进链表尾部,尾插法
AddItemToList(stu_list, &stus4->node); //添加进链表尾部,尾插法
AddItemToList(stu_list, &stus5->node); //添加进链表尾部,尾插法
PrintList(stu_list);
printf("\r\n Del node3\r\n");
DelItemFromList(&stus3->node);
PrintList(stu_list);
printf("\r\n add node1->node6\r\n");
AddItemAfter(&stus1->node, &stus6->node);
PrintList(stu_list);
printf("\r\n Sort List\r\n");
SortList(stu_list);
PrintList(stu_list);
printf("yes!\r\n");
}