- Makefile :文件 记录:源代码之间依赖关系、编译命令、怎么链接、目标文件名。
- make :命令 / 工具程序 执行逻辑:读取当前目录
Makefile,自动分析哪些文件需要重新编译,执行里面的指令
3. Makefile 基础语法格式
makefile
目标: 依赖文件
命令 # ⚠️ 前面必须是【Tab键】,不能空格!

makefile中有许多预定义变量,这些变量具有特殊的含义,可在 makefile 中直接使用。
$@ 目标名
$< 依赖文件列表中的第一个文件
$^ 依赖文件列表中除去重复文件的部分

示例:
-
自定义变量展开:`CC=gcc`、`SRC=main.c dlinklist.c`、`TARGET=dlinklist`、`CFLAGS= -Wall -g`,所有`$(变量名)`会被对应内容直接替换。
-
规则头部 `(TARGET):(SRC)` 展开为 `dlinklist:main.c dlinklist.c`,声明可执行文件依赖两份源码。
-
编译命令 `(CC) (SRC) -o @ (CFLAGS)` 逐个替换:`(CC)\`替换为gcc,\`(SRC)`替换源文件列表,内置变量`@\`代表当前目标dlinklist,\`(CFLAGS)`替换编译参数。
-
全部替换完成,最终真实执行命令:`gcc main.c dlinklist.c -o dlinklist -Wall -g`,输入make即可运行编译。
-
执行`make clean`,`rm $(TARGET)`展开为`rm dlinklist`,完成删除可执行文件操作。
CC=gcc
SRC=main.c dlinklist.c
TARGET=dlinklist
CFLAGS= -Wall -g(TARGET):(SRC)
(CC) (SRC) -o @ (CFLAGS)clean:
rm $(TARGET)
双向链表核心知识点
单向链表:只有 后继指针 next,只能向后遍历; 双向链表:每个节点拥有两个指针
typedef struct Node
{
data_t data; // 数据域
struct Node *prev; // 前驱指针,指向前一个节点
struct Node *next; // 后继指针,指向后一个节点
}node_t;
- prev:指上一个节点
- next:指下一个节点 优势:可以向前、向后双向遍历;已知某节点,直接删除该节点不需要从头查找前驱。 缺点:每个节点多占用一个指针内存。
双向链表头结点创建
-
函数启动,定义指针变量 head,调用 malloc 申请一块大小与 node_t 结构体相等的堆内存,把分配到的内存地址存入 head。
-
判断 head 的值是否等于 NULL,如果内存申请失败,打印 malloc fail 提示语句,随后返回 NULL,结束当前函数。
-
若内存分配成功,将头结点内部 next 指针赋值为 NULL,再将头结点内部 prev 指针赋值为 NULL。
-
全部赋值操作完成,函数返回头结点指针 head。
node_t * dlinklist_create(void)
{
node_t * head = malloc(sizeof(node_t));
if(head == NULL)
{
printf("malloc fail\n");
return NULL;
}
//头结点的上下指针均赋值为0
head->next = NULL;
head->prev = NULL;return head;}
双向链表头结点插入新节点
-
判断头结点指针是否为空,为空直接退出;
-
动态申请内存创建新节点,内存分配失败打印提示并返回;
-
将传入数据存入新节点的数据域;
-
新节点后继指针指向头结点原本的后继节点;
-
新节点前驱指针指向头结点;
-
更新头结点后继指针,使其指向新节点;
-
判断新节点后继不为空,链表原本存在有效结点,将原首节点的前驱指针指向新节点;
-
函数执行完毕,完成双向链表头插。
void dlinklist_insert_head(node_t *head,data_t data)
{
if(head==NULL)
{
return ;
}
//创建新节点
node_t *pNew = malloc(sizeof(node_t));
if(pNew==NULL)
{
printf("malloc fail");
return ;
}//新节点写入数据 pNew->data = data; //新节点获取头结点的next pNew->next = head->next; //新节点指向头结点 pNew->prev = head; //头结点的next指向新节点成链 head->next = pNew; //如果链表非空节点,新节点的next指向首节点地址 if(pNew->next!=NULL) { pNew->next->prev = pNew; }}
双向链表尾结点插入新节点
-
判断头结点指针是否为空,为空直接退出;
-
动态申请内存创建新节点;
-
若内存分配失败打印提示并结束函数;
-
将传入数据存入新节点数据域;
-
使用指针 p 从 head 开始向后遍历,找到链表最后一个节点;
-
新节点后继赋值为 NULL;新节点前驱指向找到的尾节点 p;
-
将尾节点 p 的后继指向新节点,完成双向链表尾插。
void dlinklist_insert_tail(node_t *head,data_t data)
{
if(head==NULL)
{
return ;
}
//创建新节点
node_t *pNew = malloc(sizeof(node_t));
if(pNew==NULL)
{
printf("malloc fail\n");
}
//新节点填充数据
pNew->data = data;if(pNew==NULL) { printf("malloc fail"); return ; } //寻找尾节点 node_t *p = head; while(p->next!=NULL) { p = p->next; } //链接新节点 pNew->next = p->next; pNew->prev = p; p->next = pNew;}
双向链表按数据内容查找地址
-
判断头结点指针是否为 NULL,指针非法直接返回 NULL;
-
判断链表是否为空链表,为空打印提示信息并返回 NULL;
-
遍历指针 p 指向第一个有效节点;
-
while 循环遍历链表所有有效节点;
-
如果当前节点数据与目标 data 相等,直接返回当前节点地址;
-
未匹配成功,p 向后移动继续循环;
-
循环结束代表遍历完整链表无匹配项,返回 NULL。
node_t *dlinklist_find_key(node_t *head,data_t data)
{
//判断链表地址合法且非空链表if(head == NULL) { return NULL; }else if(is_empty(head)==0) { printf("linklist empty\n"); return NULL; } //从首节点开始寻找 node_t *p = head->next; //遍历到尾节点为止 while(p!=NULL) { //找到对应数据节点返回节点地址 if(p->data == data) { return p; } //没找到对应节点一直向后遍历 p = p->next; } //找完整个链表也没找到匹配节点则返回空地址 return NULL;}
双向链表从头结点后删除节点
-
判断头结点指针是否为 NULL,指针非法直接函数返回;
-
判断链表为空,打印提示信息并直接返回;
-
指针 p 保存第一个有效节点地址;
-
修改头结点后继指针,指向原首节点的下一个节点;
-
如果被删节点后方还有有效节点,修改后继节点前驱指针指向头结点;
-
释放被删除节点内存,完成双向链表头结点删除。
void dlinklist_delete_head(node_t *head)
{
//判断链表地址合法且非空链表if(head == NULL) { return ; }else if(is_empty(head)==0) { printf("linklist empty\n"); return ; } node_t *p = head->next; head->next = p->next; if(p->next!=NULL) p->next->prev = head; free(p);}
双向链表从尾节点删除节点
-
判断头结点指针是否为 NULL,指针非法直接函数返回;
-
判断链表为空,打印提示信息并直接返回;
-
遍历指针 p 指向第一个有效节点;
-
循环向后移动 p,直到 p 指向链表尾部节点;
-
将尾节点前驱节点的后继指向 NULL,断开尾节点链接;
-
释放尾节点内存,完成双向链表尾删除节点。
void dlinklist_delete_tail(node_t *head)
{
//判断链表地址合法且非空链表
if(head == NULL)
{
return ;
}else if(is_empty(head)==0)
{
printf("linklist empty\n");
return ;
}node_t *p = head->next; while(p->next!=NULL) { p = p->next; } p->prev->next = p->next; free(p);}
双向链表根据数据查找删除对应节点
-
判断头结点指针是否为 NULL,指针非法直接函数返回;
-
判断链表为空,打印提示信息并直接返回;
-
遍历指针 p 指向第一个有效节点;
-
while 循环遍历链表所有有效节点;
-
如果当前节点数据与目标 key 相等: 将前驱节点后继指向当前节点的后继; 如果存在后继节点,更新后继节点的前驱; 释放当前节点内存,函数结束;
-
未匹配成功,p 向后移动继续循环;
-
遍历完整链表无匹配项,直接返回。
void dlinklist_delete_key(node_t *head,data_t key)
{
//判断链表地址合法且非空链表if(head == NULL) { return ; }else if(is_empty(head)==0) { printf("linklist empty\n"); return ; } //从首节点开始寻找 node_t *p = head->next; //遍历到尾节点为止 while(p!=NULL) { //找到对应数据节点返回节点地址 if(p->data == key) { p->prev->next = p->next; if(p->next!=NULL) { p->next->prev = p->prev; } return ; } //没找到对应节点一直向后遍历 p = p->next; } //找完整个链表也没找到匹配节点则返回空地址return ;
}
双向链表销毁
-
判断二级指针 head 是否为 NULL,参数非法直接返回;
-
判断链表为空,打印提示,释放头结点内存,外部指针置空后函数返回;
-
遍历指针 p 指向第一个有效节点;
-
循环遍历所有有效节点: 使用 temp 保存当前待释放节点; p 先向后移动保存下一个节点地址; 释放 temp 指向的有效节点;
-
所有有效节点全部释放完成后,释放头结点;
-
将外部传入的链表头指针置为 NULL,消除野指针。
void dlinklist_destroy(node_t **head)
{
//判断链表地址合法且非空链表if(head == NULL) { return ; }else if(is_empty(*head)==0) { printf("linklist empty\n"); free(*head); *head = NULL; return ; } //获取首节点 node_t *p = (*head)->next; while(p!=NULL) { //中间变量存储当前需要释放的节点 node_t *temp = p; //后移遍历链表节点 p = p->next; //释放当前位置节点 free(temp); } //释放头结点地址并将指针指向NULL防止悬空 free(*head); *head = NULL;}