前言:在链表的构建中,链表的初始化和销毁为何需要使用一个二级指针,而不是只需要传递一个指针就可以了,其问题的关键就在于c语言的参数传递的方式是值传递
那么,这篇文章就来聊一聊在链表的初始化中一级指针的传递和二级指针的区别,并总结给出单链表的C语言的增删查改操作详细代码.
开整开整:
目录
1.一级指针和二级指针的区别
函数栈帧的创建和销毁以及不同类型的变量存储区
关于NULL,你了解多少?
2.链表中的相关指针操作
一级指针+头结点初始化 = 二级指针
初始化头结点的两种方式:
1.自定义头结点初始化函数
2.手写结构体构造函数
3.单链表的基本功能实现
3.1带头节点的单链表
3.2不带头节点的链表
4.金句频道
1.一级指针和二级指针的区别
一级指针这里不再进行阐述,我们重点是来说说二级指针的原理及应用场景,我们现在试想以下的场景:
其实在大多数场景下用不到二级指针,但是,作为链表,也只是需要在特殊情况下才需要使用二级指针,
预备知识:
函数栈帧的创建和销毁以及不同类型的变量存储区
我们都知道,函数在调用时会创建栈帧,在栈帧中实现函数的相关操作,比如创建临时变量和开辟空间等操作,但是这里需要注意的是,空间开辟是在堆区,堆区不会受函数栈帧的影响,也就是说函数栈帧内部开辟的空间,是直接在堆区上开辟的,并不会随着函数栈帧的销毁而销毁,有关函数栈帧的问题,可以参考函数栈帧的创建与销毁,而函数内部创建的变量不一样,这些变量都会随着函数调用的结束,函数栈帧的销毁而销毁,并且在函数传值调用时,形参变量只是复制了一份实参变量,并不会真的拿实参变量来操作,这也就有了传值调用和传址调用的区别,我们的地址是唯一的,当我们采用传址调用时,通过地址想当于我们直接找到了实参变量,也就相当于间接操作实参。
关于NULL,你了解多少?
NULL一般表示无效地址,我们所说的置空就是将指针指向NULL,对NULL的解引用操作是不合法的,NULL其地址值为0,而由于任何进程的0地址开始存储的都是系统关键地址,比如进程的退出,堆栈维护,键盘处理等系统控制程序的地址。因此0地址是不允许用户代码中直接读写访问的(hacking除外),如果某指针被赋予NULL,之后该指针被用来操作对象或内存,要么在编译时报错,要么运行时程序崩溃。
指针被赋值为NULL的意义在于,将NULL作为唯一无效指针的标志,明确规定指针值要么为NULL要么为其他有效地址,方便后续代码判断该指针的有效性,以便代码不会访问无效地址.
所以,我们尝试通过NULL进行传参时一定要注意,我们需要注意此时函数内部的操作是否是合法的,并且,如果我们想要操作以NULL传入的变量时,在栈区上进行的操作都会随着函数栈帧的销毁而失效,其原因可以解释为是对NULL指针的传入是无效的,我们知道传入一个地址可以修改该地址上的值,但是对于传入的NULL指针来说,不管函数内部的局部变量如何操作,在这个函数结束后,都会回到传入NULL指针的状态,下面,我们给出有关二级指针的操作示例:
#include<bits/stdc++.h>
void func1(int* pp)
{
pp = (int*)malloc(sizeof(int)*10);//分配了空间,相当于有了可以辨识的唯一地址,但是由于pp传入时拷贝的是NULL指针,是无效指针,所以pp的操作不会影响到实参p
*pp = 1;
printf("%d\n", *pp);
}
void func2(int* p)
{
*p += 4;//由于p此时的地址是有效地址,所以我们相当于在操作a
int* q = p;
*q += 4;//同理,这相当于指针的传递,但是需要在实参指针有效的情况下,这里只是指出不要产生实参是什么类型,形参就要用它的指针的固化概念(当然二级指针也是对的)
}
void func3(int** p)
{
printf("%p\n", p);
printf("%p\n", *p);
*p = (int*)malloc(sizeof(int));
printf("%p\n", *p);
**p = 3;
}
int main()
{
int* p = NULL;
//这里注意&p一定不是NULL,p相当于是个指针变量,开了变量p就要有有效的地址,只是这个地址上存储的是无效的地址罢了
func1(p);
printf("%p\n", p);
func3(&p);
printf("%p\n", p);
printf("%d\n", *p);
printf("%d\n", *p);
int a = 3;
int* pp = &a;
func2(pp);
printf("%d\n", *pp);
return 0;
}
我们有如下的运行结果:
下面是二级指针和一级指针的示意图:
可见,我们操作NULL指针的唯一的途径,就是传NULL指针的指针,这些我们会在接下来的链表操作中用到,希望读者理解。
2.链表中的相关指针操作
通过上述的分析,我们将其应用到链表中,来思考以下几个问题:
1.如何初始化链表才能避免对NULL指针的操作,正常的操作链表?
2.链表添加头结点和不添加头结点有什么不同之处?
3.有没有方法可以在使用一级指针的情况下能够正常操作链表?
首先,我们在前面说,对操作NULL指针的唯一方法就是传二级指针,那么我们在链表中,也需要传二级指针,所以我们在设计参数时,要传入的就变成了结构体的二级指针,对二级指针变量解引用才是合法的操作,具体可看下面的示例:
void LinkedListDestroy(SingleNodeList **head){//7释放链表
SingleNodeList *p=*head,*q;
while(p!=NULL){
q=p;
p=p->next;
free(q);
}
*head=NULL;
}
是不是所有的操作都需要二级指针?
并不是这样的,在链表的操作中,有很多不会改变链表状态的函数,如打印输出函数,寻找特定值得函数,这些函数并不会操作和改变链表结构,所以并不需要二级指针,当然,在前面,我们的例子中也看出,有些操作直接采用一级指针的传递就可以进行,当然,这需要一个很重要的前提:实参一定不是NULL,实参一定不是NULL,实参一定不是NULL,这个前提很重要,将在后面展开。
头结点的意义在哪?
我们在链表函数的设计时,经常需要考虑链表是不是为空,而头结点存在的意义就是为了消除这个考虑因素,通过在链表头部增加一个虚拟节点(实际存在,但是我们并不会使用)来使链表的操作更加统一和简便,这样,我们在空链表的时候,就不会在出现对NULL指针的错误解引用操作了,因为此时我们的链表至少存在一个头结点了。
一级指针+头结点初始化 = 二级指针
从前面的例子中我们不难看出,二级指针是适用于实参是NULL的情况与实参是正常有效指针的情况下的统一操作,那么我们就可以直接通过设置头结点来将链表初始化为不为NULL的指针,这样就不会在出现,对NULL指针的误解引用了,哼哼,终于不用写两个*了~~~
哎~~等等,先回来,我再问一句,那头结点怎么开辟呢?(对不起,二级指针,我错了,没了你我怎么初始化啊,回来吧)
庆幸的是,我们这里只需要在初始化头结点的时候用二级指针就好了,其他的情况下,直接用头结点指针操作就好了。
初始化头结点的两种方式:
1.自定义头结点初始化函数
// 初始化链表法,创建空头结点
void InitSList(SLTNode** head)
{
*head = (SLTNode*)malloc(sizeof(SLTNode));
(* head)->data = -1;
(* head)->next = NULL;
}
你初始化数据域为-1,那万一我链表中的值就是-1怎么办?
你这问题问的好(其实是我自己想说),我们的真正的数据域要想和头节点的数据域区分的话还是比较简单的,其一,我们在操作时可以使指向链表的指针直接从第二个结构体开始遍历,直接跳过头结点,其二,如果某些操作下必须从头结点开始遍历,那也可以直接在结构体上再加一个标记,将头结点标记一下,一般还是不需要这样操作的。
2.手写结构体构造函数
会c++的朋友你们有福了,我们只需要手写一个结构体内部的构造函数来实现初始化头结点的功能,如下:
头结点初始化函数直接两行结束:
//构造函数法创建头结点
SLTNode* head = NULL;
//采用构造函数实现方式,初始化头结点为-1和NULL
head=new SLTNode;
3.单链表的基本功能实现
3.1带头节点的单链表
//SList.h
#pragma once //防止头文件重复包含
//头文件的包含
#include <stdio.h>
#include <stdlib.h>
#include <assert.h>
//符号和结构的声明
typedef int SLTDataType; //数据类型重命名
typedef struct SListNode //链表的一个节点
{
SLTDataType data;
struct SListNode* next; //存放下一个节点的地址
struct SListNode(SLTDataType d=-1, SListNode* p = NULL)//构造函数法创建头结点(C++)
{
data = d;
next = p;
}
}SLTNode;//如果这里可以写SLTNode,*SLT;那么所有的二级指针都可以替换成SLT*
//函数的声明
// 初始化链表,创建空头结点
void InitSList(SLTNode** head);
//创建新建节点
SLTNode* BuySLTNode(SLTDataType x);
//在头部插入数据
void SListPushFront(SLTNode* pphead, SLTDataType x);
//销毁链表
void SListDestory(SLTNode* pphead);
//在尾部插入数据
void SListPushBack(SLTNode* pphead, SLTDataType x);
//查找指定数据
SLTNode* SListFind(SLTNode* phead, SLTDataType x);
//在pos之前插入数据
void SListInsert(SLTNode* pphead, SLTNode* pos, SLTDataType x);
//在pos之后插入数据
void SListInsertAfter(SLTNode* pphead, SLTNode* pos, SLTDataType x);
//打印链表
void SListPrint(SLTNode* phead);
//在头部删除数据
void SListPopFront(SLTNode* pphead);
//在尾部删除数据
void SListPopBack(SLTNode* pphead);
//删除pos位置处的数据
void SListErase(SLTNode* pphead, SLTNode* pos);
//删除pos位置后的数据
void SListEraseAfter(SLTNode* pphead, SLTNode* pos);
//修改pos位置处的函数
void SListModify(SLTNode* phead, SLTNode* pos, SLTDataType x);
//SList.cpp
#include "Slist.h"
// 初始化链表法,创建空头结点
void InitSList(SLTNode** head)
{
*head = (SLTNode*)malloc(sizeof(SLTNode));
(* head)->data = -1;
(* head)->next = NULL;
}
//创建新建节点
SLTNode* BuySLTNode(SLTDataType x)
{
//手动创建
// SLTNode* newnode= (SLTNode*)malloc(sizeof(SLTNode));
//newnode->data = x;
//newnode->next = NULL;
//构造函数创建
SLTNode* newnode = new SLTNode(x);//其他未传入的变量都初始化为默认值
return newnode;
}
//在头部插入数据
void SListPushFront(SLTNode* pphead, SLTDataType x)
{
SLTNode* p = pphead;
SLTNode* q = p->next;
p->next = BuySLTNode(x);
p->next->next = q;//链接
}
//销毁链表
void SListDestory(SLTNode* pphead)
{
//注意,这里我们认为销毁链表不会销毁头结点,所以我们要从第二个也即有效数据节点开始
SLTNode* p = pphead->next;
if (!p)
return;
else
{
while (p)
{
SLTNode* temp = p->next;//先记住要销毁节点的下一个节点,避免链接断开导致无法删除
free(p);
p = NULL;
p = temp;
}
}
}
//在尾部插入数据
void SListPushBack(SLTNode* pphead, SLTDataType x)
{
SLTNode* p = pphead;
while (p->next)//这里要注意不能写while(p)这样会导致插入的数据链接不到链表上,原因是在函数内部变量的东西都会随着函数的结束而销毁,创建的空间随之也会内存泄漏,具体可以画物理图看看
{
p = p->next;
}
p->next = BuySLTNode(x);
}
//查找指定数据
SLTNode* SListFind(SLTNode* phead, SLTDataType x)
{
SLTNode* p = phead->next;
while (p)
{
if (p->data == x)
return p;
p = p->next;
}
//如果查找不到,则返回NULL
return NULL;
}
//在pos之前插入数据
void SListInsert(SLTNode* pphead, SLTNode* pos, SLTDataType x)
{
SLTNode* p = pphead->next;
while (p->next!=pos&&p)//找到pos前面的节点
{
p = p->next;
}
//如果找不到pos,返回NULL
if (p == NULL)
return;
else
{
p->next = BuySLTNode(x);
p->next->next = pos;
}
}
//在pos之后插入数据
void SListInsertAfter(SLTNode* pphead, SLTNode* pos, SLTDataType x)
{
SLTNode* p = pphead->next;
while (p != pos&&p)//找到pos节点处
{
p = p->next;
}
if (p == NULL)//没找到pos
return;
else
{
SLTNode* q = p->next;//保存pos后面的节点
p->next = BuySLTNode(x);
p->next->next = q;
}
}
//打印链表
void SListPrint(SLTNode* phead)
{
SLTNode* p = phead->next;//我们创建的头结点不需要打印
while (p->next)//这里的目的只是为了好看,中间可以加个箭头
{
printf("%d->", p->data);
p = p->next;
}
printf("%d", p->data);
printf("->NULL\n");
}
//在头部删除数据
void SListPopFront(SLTNode* pphead)
{
SLTNode* p = pphead->next;
if (!p)
{
printf("当前链表为空\n");
return;
}
else
{
//防止内存泄漏,我们要将第一个数据free掉
SLTNode* q = p->next;
free(p);
p = NULL;
pphead->next = q;
}
}
//在尾部删除数据
void SListPopBack(SLTNode* pphead)
{
SLTNode* p = pphead->next;
if (!p)
{
printf("当前链表为空\n");
return;
}
else if (p->next == NULL)//只有一个节点(不算头结点)
{
free(p);
p = NULL;
}
else //两个及以上节点数
{
while (p->next->next)//这里我们要在删除最后一个节点的同时将原倒数第二个节点的next置空,所以我们需要一次向后看两个节点
{
p = p->next;
}
free(p->next);
p->next = NULL;
}
}
//删除pos位置处的数据
void SListErase(SLTNode* pphead, SLTNode* pos)
{
SLTNode* p = pphead->next;
while (p->next!= pos&&p)
{
p = p->next;
}
if (!p)
{
printf("找不到节点pos\n");
return;
}
else
{
SLTNode* q = p->next->next;//记住pos后面的节点
free(p->next);
p->next = q;
}
}
//删除pos位置后的数据
void SListEraseAfter(SLTNode* pphead, SLTNode* pos)
{
SLTNode* p = pphead->next;
while (p!= pos && p)
{
p = p->next;
}
if (!p)
{
printf("找不到节点pos\n");
return;
}
else
{
SLTNode* q = p->next->next;//记住pos后面的后面的数据
free(p->next);
p->next = q;
}
}
//修改pos位置处的函数
void SListModify(SLTNode* phead, SLTNode* pos, SLTDataType x)
{
SLTNode* p = phead->next;
while (p != pos && p)
{
p = p->next;
}
if (!p)
{
printf("找不到节点pos\n");
return;
}
{
p->data = x;
}
}
//测试代码,样例均为合法样例,其他样例均为测试,如果发现bug还望指出,感谢
#include <bits/stdc++.h>
#include "Slist.h"
//测试代码
int main()
{
//构造函数法创建头结点
SLTNode* head = NULL;
//采用构造函数实现方式,初始化头结点为-1和NULL
head=new SLTNode;
//手动修改为头结点,因为我们的构造函数后序可能还会用到
//采用自写初始化函数的方式创建头几点,注意NULL指针初始时不能解引用,所以我们只能用二级指针
//InitSList(&head);
SListPushFront(head, 1);
SListPushFront(head, 2);
SListPushBack(head, 3);
SListPushBack(head, 4);
SLTNode* p = SListFind(head, 3);
if (p)
printf("%d\n", p->next->data);
SListInsert(head, p, 5);
SListInsertAfter(head, p, 6);
SListInsertAfter(head, p, 6);
SListPopFront(head);
SListPopBack(head);
SListModify(head,p, 4);
SListEraseAfter(head, p);
SListErase(head, p);
SListPrint(head);
return 0;
}
3.2不带头节点的链表
终究还是逃不过二级指针......,咳咳,开个玩笑,二级指针很好理解的,现在早早学会了就不怕了,那不是更好嘛?这里我就偷个懒,只给出声明和实现啦,嘿嘿
// slist.h
typedef int SLTDateType;
typedef struct SListNode
{
SLTDateType data;
struct SListNode* next;
}SListNode;
// 动态申请一个节点
SListNode* BuySListNode(SLTDateType x);
// 单链表打印
void SListPrint(SListNode* plist);
// 单链表尾插
void SListPushBack(SListNode** pplist, SLTDateType x);
// 单链表的头插
void SListPushFront(SListNode** pplist, SLTDateType x);
// 单链表的尾删
void SListPopBack(SListNode** pplist);
// 单链表头删
void SListPopFront(SListNode** pplist);
// 单链表查找
SListNode* SListFind(SListNode* plist, SLTDateType x);
// 单链表在pos位置之后插入x
void SListInsertAfter(SListNode* pos, SLTDateType x);
// 单链表删除pos位置之后的值
void SListEraseAfter(SListNode* pos);
// 单链表的销毁
void SListDestroy(SList* plist);
// slist.c
#include "SList.h"
SListNode* BuySListNode(SLTDateType x)
{
SListNode* node = (SListNode*)malloc(sizeof(SListNode));
node->data = x;
node->next = NULL;
return node;
}
void SListPrint(SListNode* plist)
{
SListNode* cur = plist;
while (cur)
//while (cur != NULL)
{
printf("%d->", cur->data);
cur = cur->next;
}
printf("NULL\n");
}
void SListPushBack(SListNode** pplist, SLTDateType x)
{
SListNode* newnode = BuySListNode(x);
if (*pplist == NULL)
{
*pplist = newnode;
}
else
{
SListNode* tail = *pplist;
while (tail->next != NULL)
{
tail = tail->next;
}
tail->next = newnode;
}
}
void SListPopBack(SListNode** pplist)
{
SListNode* prev = NULL;
SListNode* tail = *pplist;
// 1.空、只有一个节点
// 2.两个及以上的节点
if (tail == NULL || tail->next == NULL)
{
free(tail);
*pplist = NULL;
}
else
{
while (tail->next)
{
prev = tail;
tail = tail->next;
}
free(tail);
tail = NULL;
prev->next = NULL;
}
}
void SListPushFront(SListNode** pplist, SLTDateType x)
{
assert(pplist);
// 1.空
// 2.非空
SListNode* newnode = BuySListNode(x);
if (*pplist == NULL)
{
*pplist = newnode;
}
else
{
newnode->next = *pplist;
*pplist = newnode;
}
}
void SListPopFront(SListNode** pplist)
{
// 1.空
// 2.一个
// 3.两个及以上
SListNode* first = *pplist;
if (first == NULL)
{
return;
}
else if (first->next == NULL)
{
free(first);
*pplist = NULL;
}
else
{
SListNode* next = first->next;
free(first);
*pplist = next;
}
}
SListNode* SListFind(SListNode* plist, SLTDateType x)
{
SListNode* cur = plist;
while (cur)
{
if (cur->data == x)
return cur;
cur = cur->next;
}
return NULL;
}
void SListInsertAfter(SListNode* pos, SLTDateType x)
{
assert(pos);
SListNode* next = pos->next;
// pos newnode next
SListNode* newnode = BuySListNode(x);
pos->next = newnode;
newnode->next = next;
}
void SListEraseAfter(SListNode* pos)
{
assert(pos);
// pos next nextnext
SListNode* next = pos->next;
if (next != NULL)
{
SListNode* nextnext = next->next;
free(next);
pos->next = nextnext;
}
}
4.金句频道
初中,你哭了,整个班级都围过来问你怎么了。高中,你难过,几个死党摸摸,告诉你还有我们没关系。大学,没人管你怎么样。工作了,人家只会觉得你演技很棒。其实,成长就是逼着你一个人去坚强。只有当你变强大,你才不害怕孤单,当你变好,你才会遇到更好的。