最近在編寫一個鏈表的時候遇到了關於指針的一些問題,在研究的過程中終於弄懂了在函數形參中使用二重指針的意義和用法。
我們先從編寫一個函數說起。這個函數要求在一個鏈表的頭部插入一個節點,這個鏈表沒有頭結點,並且要求返回值是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=%d\n",a,b);
Swap(a,b);
printf("a=%d,b=%d\n",a,b);
return 0;
}
輸出結果是:
1,2
1,2
結果是沒有成功交換。我們來看一下其中的內存空間分配以及變量交換出現了什么問題。
注意這張圖里的黑色和紅色的變量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=%d\n",a,b);//在函數中輸出
}
int main()
{
int a=1;
int b=2;
printf("a=%d,b=%d\n",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=%d\n",a,b);
Swap(&a,&b);
printf("a=%d,b=%d\n",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=%d\n",a,b);
Swap(&a,&b);
printf("a=%d,b=%d\n",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=%d\n",a,b);
Swap(&a,&b);
printf("a=%d,b=%d\n",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指向的地址,即賦值給指向鏈表第一個節點的指針
}
}
用文字說還是很繞,上圖:
終於大功告成,至此我們終於理解了二重指針的作用。然而如果允許返回一個指針,那么其實事情本可以更簡單,我們也就不用使用二重指針了。