一個例項搞懂二重指標
最近在編寫一個連結串列的時候遇到了關於指標的一些問題,在研究的過程中終於弄懂了在函式形參中使用二重指標的意義和用法。
我們先從編寫一個函式說起。這個函式要求在一個連結串列的頭部插入一個節點,這個連結串列沒有頭結點,並且要求返回值是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
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=%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指向的地址,即賦值給指向連結串列第一個節點的指標
}
}
用文字說還是很繞,上圖:
終於大功告成,至此我們終於理解了二重指標的作用。然而如果允許返回一個指標,那麼其實事情本可以更簡單,我們也就不用使用二重指標了