一个实例搞懂二重指针
- 2019 年 10 月 3 日
- 笔记
最近在编写一个链表的时候遇到了关于指针的一些问题,在研究的过程中终于弄懂了在函数形参中使用二重指针的意义和用法。
我们先从编写一个函数说起。这个函数要求在一个链表的头部插入一个节点,这个链表没有头结点,并且要求返回值是void。也就是说在函数里要完成对链表头指针的修改。
一开始我的写法是这样的:
typedef struct ListNode{ int val; struct ListNode* next; }ListNode; void myLinkedListAddAtHead(ListNode* obj,int val){ ListNode *List=obj; ListNode *Temp=malloc(sizeof(ListNode)); if(temp==NULL){ prinf("Out of space!"); } else{ Temp->val=val; Temp->next=List; obj=Temp; } }
读者可以先自己想想这个函数有什么问题。我们先抛开这个例子不谈,看一下另一个简单的例子。现在要设计一个函数交换a,b的数值。
第一种写法是直接用两个变量进行传参,交换,在父函数里打印。
void Swap(int a,int b) { int tmp = a; a = b; b = tmp; } int main() { int a=1; int b=2; printf("a=%d,b=%dn",a,b); Swap(a,b); printf("a=%d,b=%dn",a,b); return 0; }
输出结果是:
1,2
2,1
结果是没有成功交换。我们来看一下其中的内存空间分配以及变量交换出现了什么问题。
注意这张图里的黑色和红色的变量a,b虽然名字相同,却是两个不同的内存空间。因为函数没有返回值,所以我们仅仅是改变了函数内部的红色a,b的值,主函数中黑色a,b的值没有改变。所以在主函数中打印时a,b的值并没有变化。
所以如果我们想成功输出的话,就要在函数内部进行输出:
void Swap(int a,int b) { int tmp = a; a = b; b = tmp; printf("a=%d,b=%dn",a,b);//在函数中输出 } int main() { int a=1; int b=2; printf("a=%d,b=%dn",a,b); Swap(a,b); return 0; }
输出结果是:
1,2
2,1
结果是成功交换。当然黑色的a,b变量仍未交换,我们只是打印出了交换后的红色变量的值。
那么我们就是想要交换a,b的值该怎么做呢?我们很自然的想到既然刚才黑色与红色是两块存储空间所以导致没有成功,那我们让他们变成同一块存储空间不就行了吗?所以第二种做法就是将变量的地址传入函数。
void Swap(int *p1,int *p2) { int *tmp = p1; p1 = p2; p2 = tmp; } int main() { int a=1; int b=2; printf("a=%d,b=%dn",a,b); Swap(&a,&b); printf("a=%d,b=%dn",a,b); return 0; }
输出结果是:
1,2
1,2
还是不行。这又是为什么呢?我们来分析一下内存分配的情况。
原来我们虽然传入了地址,但是函数内部只是交换了指针指向的变量地址,a,b的值依然未被改变。所以我们要交换的不是指针,而是指针指向地址处的值(*p1和*p2)。
void Swap(int *p1,int *p2) { int *tmp; *tmp = *p1; *p1 = *p2; *p2 = *tmp; } int main() { int a=1; int b=2; printf("a=%d,b=%dn",a,b); Swap(&a,&b); printf("a=%d,b=%dn",a,b); return 0; }
运行过程中程序又崩溃了。原来tmp是个野指针,而*tmp是计算机系统中一个随机地址所存储的int数值。直接修改会造成难以预料的错误。所以我们直接用一个int型的tmp变量代替*tmp就好了,即下图。
void Swap(int *p1,int *p2) { int tmp; tmp = *p1; *p1 = *p2; *p2 = tmp; } int main() { int a=1; int b=2; printf("a=%d,b=%dn",a,b); Swap(&a,&b); printf("a=%d,b=%dn",a,b); return 0; }
兜兜转转之后,让我们回到最初的问题里,你发现问题所在了吗?没错,参数里的obj就相当于第一种写法中红色的变量a,他保存的只是指向原链表第一个节点的指针的一个副本。也就是说函数内部开辟了一块指针大小的空间,然后将链表头的地址复制到这个空间里了。对这个函数内部的空间的操作完全与原链表头无关。
所以根据之前例子中的做法,我们把这里的ListNode*当成int来看,就会发现我们应该传入的是ListNode*的地址,即ListNode**了。这就是二重指针的由来,我们要改变指针的地址了。我们的最终写法就是:
typedef struct ListNode{ int val; struct ListNode* next; }ListNode; void myLinkedListAddAtHead(ListNode** obj,int val){ ListNode *List=*obj;//obj存储的是指向链表第一个节点的指针的地址,List存储obj地址中保存的值,即链表第一个节点的地址 ListNode *Temp=malloc(sizeof(ListNode)); if(temp==NULL){ prinf("Out of space!"); } else{ Temp->val=val; Temp->next=List; *obj=Temp;//将新节点的地址赋值给obj指向的地址,即赋值给指向链表第一个节点的指针 } }
用文字说还是很绕,上图:
终于大功告成,至此我们终于理解了二重指针的作用。然而如果允许返回一个指针,那么其实事情本可以更简单,我们也就不用使用二重指针了。