在C语言学习中,当刚学习到二级指针的时候,我们很容易可以理解它的含义,无非就是存放一级指针变量的地址,对他进行解引用可以得到一级指针,但是我们并不能想象到它的用处。
最近在学习数据结构和算法中的链表时,在写单链表的尾插函数时,我遇到了二级指针的真实应用场景,讲解如下。
问题的出现
在单链表的尾插函数中,按照正常的编程思路,要判断传进来的链表是不是空的,如果链表为空(phead == NULL),则应该新建一个结点,然后把链表头指针指向这个新结点;如果链表不为空,则应该先寻找该链表的尾部,再把尾部的next指针指向新结点。按照这个思路,我们很容易写出如下代码:
// 在单链表末尾添加元素
void SListPushBack(SListNode *phead, SListDataType x) {
// 初始化新节点
SListNode *tail = CreatSListNode(x);
// 当链表为空,phead==NULL
if(NULL == phead){
phead = tail;
}
else{
SListNode *current = phead;
// 找尾部
while (current->next != NULL) {
current = current->next;
}
// 添加新结点到尾部
current->next = tail;
}
}
这个代码乍一看没什么问题,先把新结点初始化,然后判断当前链表是否为空,为空则把头指针指向新节点,不为空则把尾部结点的next指向新节点,但是它犯了一个巨大的错误,就是形参和实参的问题。
我们先看一下当初指针是怎么被提出的。
追溯:指针的提出
记得在大多数课程中,指针的提出都来自于一个很经典的问题:用一个函数来交换两个数。新手拿到这个问题,很容易写出下面的代码:
void swap(int a, int b){
int tmp;
tmp = a;
a = b;
b = tmp;
}
但是这样就落入了形参和实参的陷阱,传进来的参数并不会参与操作,而是会在函数内部新建参数,把传过来的参数复制过去,然后进行函数内部的操作,等到函数结束,函数内部的参数被销毁,整个函数相当于啥都没做。
因此,提出了指针的概念,既然会创建一个临时变量,那我们能不能拿到原始的数据并修改它呢,指针便是这个用处,这一块便不详细讲了,想要学习指针的用处,可以查看其它博客。
最终修改后的swap函数为:
void swap(int *a, int *b){
int tmp;
tmp = *a;
*a = *b;
*b = tmp;
}
这样便能达到交换两个数据的目的。
将二级指针与一级指针类比
从上面的代码可以得到一级指针的用处:当我们需要更改一个数据的时候,用指针解引用,可以找到需要修改的数据的位置,然后进行修改,再回到刚才写错的单链表尾插函数,分两部分来看,先看后半部分:
else{
SListNode *current = phead;
// 找尾部
while (current->next != NULL) {
current = current->next;
}
// 添加新结点到尾部
current->next = tail;
}
这一部分的代码其实是没有问题的,在 else 分支里,SListNode *current = phead;。虽然 current 也是形参,但它获取到了那个真实结构体的地址(phead),执行 current->next = tail; 时,修改的是内存中那个结构体实体内部的 next 成员,而不是修改 current 指针本身。所以即使 current 被销毁,内存里的结构体已经被修改成功了。但是前半部分的问题就大了:
if(NULL == phead){
phead = tail;
}
这里并不是修改指针指向位置的值,而是直接修改了指针,按照前面错误的swap函数的讲解,我们可以知道,函数内部的指针变量,也是一个形参,它相当于在函数内部新建了一个指针变量来存放我们传给他的指针,因此phead = tail这个改变指针的操作,只是改变的形参,传过来的指针并没有被改变!这样函数结束的时候,函数内部的变量被销毁,便什么都没有改变,和上面的swap函数犯了同样的错误。
由此,便引出了二级指针,既然我们要在函数内部改变指针的值,那对这个指针取地址,得到二级指针,我们只需要改变二级指针指向空间的值(也就是一级指针的值),不就真正改变了一级指针的值嘛,因此,正确的尾插代码如下:
// 在单链表末尾添加元素
void SListPushBack(SListNode **pphead, SListDataType x) {
assert(pphead != NULL);
// 初始化新节点
SListNode *tail = CreatSListNode(x);
// 当链表为空,*pphead==NULL
if(NULL == *pphead){
*pphead = tail;
}
else{
SListNode *current = *pphead;
// 找尾部
while (current->next != NULL) {
current = current->next;
}
// 添加新结点到尾部
current->next = tail;
}
}