為什么要使用二級指針?
掃描二維碼
隨時(shí)隨地手機(jī)看文章
筆者能力有限,如果文中出現(xiàn)錯(cuò)誤的地方,歡迎各位朋友給我指出來,我將不勝感激,謝謝~
概念
提到指針,我們都知道指針是用來存儲一個(gè)變量的地址。所以,當(dāng)我們定義了一個(gè)指向指針的指針的時(shí)候(pointer to pointer),我們也稱之為二級指針,那針對于這個(gè)二級指針來說,第一級指針存放的是指向的變量的地址,第二級指針存放的是第一級指針的地址??梢杂孟旅孢@張圖表示他們之間的關(guān)系。
上圖所表達(dá)的意思也就是,一級指針變量 ptr1 存放的是 var 變量的地址,二級指針變量 ptr2 存放的是一級指針變量的地址。這也就是關(guān)于二級指針的相關(guān)概念。
一級指針與二級指針關(guān)系示例
#include <stdio.h>
int main(void)
{
int a = 10;
int *p = &a;
int **q = &p;
printf("a = %d\n",a);
printf("&a = %p\n",&a);
printf("p = %p\n",p);
printf("&p = %p\n",&p);
printf("*p = %d\n",*p);
printf("q = %p\n",q);
printf("&q = %p\n",&q);
printf("*q = %p\n",*q);
printf("**q = %d\n",**q);
}
下圖是代碼運(yùn)行的結(jié)果:
結(jié)果也很明顯了,一級指針變量 p 存放的是變量 a 的地址,二級指針變量 q 存放的是一級指針變量 p 的地址,所以根據(jù)以上結(jié)果也能得出下面的等式:
q = &p;
*q = p = &a;
**q = *p = a;
在了解了上述一級指針和二級指針的一個(gè)關(guān)系之后,我們再來看另外一個(gè)例子:
現(xiàn)在有如下代碼:
int main(void)
{
int **ipp;
int i = 5,j = 6,k = 7;
int *ip1 = &i,*ip2 = &j;
}
如果這個(gè)時(shí)候,我們加了這么一句代碼:
ipp = &ip1;
那么上述所涉及到的數(shù)據(jù)之間的關(guān)系是這樣的:
根據(jù)上面這個(gè)圖我們也可以知道,對于 ipp 的兩次解引用的結(jié)果是 i 的值,也就是說 **ipp = 5,我想對于這個(gè)的理解并不困難,如果我繼續(xù)在這個(gè)基礎(chǔ)上添加代碼,注意,是在上條代碼的基礎(chǔ)上添加如下代碼:
*ipp = ip2;
在這條代碼的作用下,數(shù)據(jù)關(guān)系圖就發(fā)生了改變,改變?nèi)缦滤荆?br style="box-sizing: border-box;font-size: inherit;color: inherit;line-height: inherit;">
對于上述的變化來說,我們增加的代碼改變的是 *ipp 的值,也就是說 ipp 的值是不會發(fā)生改變的,既然 ipp 的值不會發(fā)生改變,那么 ipp 指向 ip1 的關(guān)系不會發(fā)生改變,我們增加的代碼改變了 *ipp 的值,那么也就是說改變了一級指針指向的值,而 ip2 是指向 j 的,所以也就有了上述的變化。
緊接著我們繼續(xù)在第一條增加的代碼的基礎(chǔ)上重新增加一條代碼,增加的代碼如下:
*ipp = &k;
那么這個(gè)時(shí)候所對應(yīng)的數(shù)據(jù)關(guān)系圖如下圖所示:
這個(gè)原理和剛才的一樣,不在這里贅述了。
二級指針的應(yīng)用
那再講述了上述的基本概念之后,我們知道二級指針變量是用于存放一級指針變量的地址的,那么在具體的實(shí)際應(yīng)用中,又在什么地方可以用到二級指針呢?下面來看一個(gè) C 語言函數(shù)傳址調(diào)用的例子。
我們在剛學(xué)習(xí)指針的時(shí)候,都會碰到如下這樣一個(gè)例子:
void swap(int *a,int *b)
{
int temp;
temp = *a;
*a = *b;
*b = temp;
}
之所以在定義函數(shù)時(shí),把函數(shù)的形參定義為指針,而非如下這樣的形式:
void swap(int a,int b);
是因?yàn)镃 語言在進(jìn)行函數(shù)調(diào)用的時(shí)候,是將實(shí)參的值復(fù)制一份,并將其副本傳遞到函數(shù)調(diào)用里,如果形參定義的不是指針,那么在函數(shù)內(nèi)部改變數(shù)值,不會對實(shí)參本來的值發(fā)生改變。而將形參定義成了指針的話,那么傳到函數(shù)里面的值雖然是實(shí)參地址的一個(gè)副本,但是地址里存的值發(fā)生了改變,也就導(dǎo)致實(shí)參本來的值也發(fā)生了改變。
有了上述分析的基礎(chǔ)上,我們知道,如果要在一個(gè)函數(shù)內(nèi)改變一個(gè)數(shù)的值,那么就需要將形參定義為指針。同樣的,如果我們要在一個(gè)函數(shù)內(nèi)改變一個(gè)指針的值,我們就需要將形參定義了二級指針,下面來看這樣一個(gè)例子:
#include <stdlib.h>
int allocstr(int len,char **retptr)
{
char *p = malloc(len + 1);/*加 1 是為了 '\0' */
if (p = NULL)
return 0;
*retptr = p;
return 1;
}
在調(diào)用的時(shí)候,是像下面這樣子進(jìn)行調(diào)用的:
char *string = "hello world!"
char *copystr;
if (allostr(strlen(string),©str))
strcpy(copystr,string);
else
printf("out of memory!\n");
上述這個(gè)例子就是涉及到字符串拷貝的一個(gè)實(shí)際的例子,因?yàn)槲覀円?nbsp;allostr
里改變指針變量 copystr 的值(要使用 malloc 分配內(nèi)存),那么就需要把 copystr 的地址傳到函數(shù)里,那么這個(gè)時(shí)候,所定義的函數(shù)形參也就需要是二級指針了。
二級指針在單鏈表中的應(yīng)用
首先,我們有這樣一個(gè)單鏈表的數(shù)據(jù)結(jié)構(gòu):
typedef struct ListNode
{
int data;
struct ListNode *next;
}ListNode;
依據(jù)這樣一個(gè)數(shù)據(jù)結(jié)構(gòu),假定我們創(chuàng)建了一個(gè)如下所示的一個(gè)單鏈表:
那么我們?nèi)绻獎h除鏈表中的一個(gè)結(jié)點(diǎn)的時(shí)候,第一時(shí)間采用的可能是如下所示的代碼:
ListNode *find_and_delete(ListNode *head,int target)
{
ListNode *pre = NULL;
ListNode *entry;
for (entry = head; entry != NULL; entry = entry->next)
{
if (entry->data == target)
{
/* 判斷刪除的結(jié)點(diǎn)是否是第一個(gè)結(jié)點(diǎn)*/
if (entry == head)
head = entry->next;
else
pre->next = entry->next;
free(entry);
break;
}
pre = entry;
}
return head;
}
上述代碼所述的刪除結(jié)點(diǎn)的思路遵循如下圖所示的原理,首先是關(guān)于當(dāng)所要刪除的結(jié)點(diǎn)是第一個(gè)結(jié)點(diǎn)的時(shí)候,刪除結(jié)點(diǎn)示意圖如下所示:
如果要刪除的結(jié)點(diǎn)不是處在第一個(gè)結(jié)點(diǎn)的位置,那么刪除結(jié)點(diǎn)的原理示意圖如下圖所示:
上述就是一個(gè)使用一級指針操作鏈表的一個(gè)簡單地例子,自己在理解這個(gè)例子的時(shí)候,也存在幾個(gè)對我來說的難點(diǎn),筆者寫下來和大家分享一下,首先,
第一個(gè)難點(diǎn)就是頭指針,在圖中畫的頭指針指向了第一個(gè)結(jié)點(diǎn),圖中所示的頭指針并沒有數(shù)據(jù)域,只是單單地指向了第一個(gè)結(jié)點(diǎn),在代碼中的 head 指針變量卻有數(shù)據(jù)域,并且就是第一個(gè)結(jié)點(diǎn)的數(shù)據(jù),這個(gè)概念的理解其實(shí)是對于指針的理解,head 指向了第一個(gè)結(jié)點(diǎn),一定注意在這里的 head 是頭指針,并不是頭結(jié)點(diǎn)。(這是筆者個(gè)人的理解,如果大家有不同的看法,歡迎各位朋友添加筆者微信共同探討)。
第二個(gè)難點(diǎn)就是上述函數(shù)中,函數(shù)有一個(gè)返回值,返回了頭指針。為什么要返回呢?是因?yàn)楫?dāng)前傳入函數(shù)的形參是一級指針,在函數(shù)內(nèi)部改變 head ,在函數(shù)運(yùn)行結(jié)束時(shí),head 值并不會發(fā)生改變,所以要返回。
第三個(gè)難點(diǎn),那么為什么鏈表操作中,又能夠刪除中間的結(jié)點(diǎn)呢?是因?yàn)殡m然 傳進(jìn)去的 head 是一級指針,但是 head 結(jié)構(gòu)體成員內(nèi)的 next 是一個(gè)指針,那這樣的話,對于 next 成員來說它是一個(gè)二級指針,對于他的變化,在函數(shù)結(jié)束時(shí)是會產(chǎn)生改變的,所以可以刪除中間的結(jié)點(diǎn)。
二級指針在單鏈表結(jié)點(diǎn)刪除的應(yīng)用
上面的例子中,在刪除單鏈表的結(jié)點(diǎn)的時(shí)候,我們形參采用的是一級指針的方式,在這個(gè)過程中,還需要引入 pre 指針來解決這個(gè)問題,還有一種很巧妙的方法,利用了二級指針的特性解決了結(jié)點(diǎn)刪除的問題,在這個(gè)過程中,運(yùn)用二級指針,不需要進(jìn)行刪除第一個(gè)結(jié)點(diǎn)的判斷。具體代碼如下:
void find_and_delete2(ListNode **head,int target)
{
for (; *head != NULL; head = &(*head)->next)
{
if ((*head)->data == target)
{
(*head) = (*head)->next;
break;
}
}
}
上述的代碼沒有創(chuàng)建任何局部變量,直接利用 head 進(jìn)行遍歷鏈表,因?yàn)槠涫嵌壷羔?,這樣子進(jìn)行遍歷在函數(shù)結(jié)束后不會改變其本身的鏈表結(jié)構(gòu)。然后,在進(jìn)行刪除的時(shí)候,(*head) 在函數(shù)結(jié)束后是會保持其在函數(shù)內(nèi)的變化值的,所以也就完成了結(jié)點(diǎn)的刪除。
結(jié)論
上述就是關(guān)于二級指針的相關(guān)內(nèi)容,總體來說,二級指針也是指針,用指針的思想來處理這個(gè)問題就好,區(qū)別只是在于一級指針是由于存放普通變量的地址的,二級指針是用于存放指針變量的地址的。另外需要注意的就是 C 語言在進(jìn)行函數(shù)調(diào)用時(shí),實(shí)參的傳遞采用的是實(shí)參值的一份拷貝。如果要在函數(shù)內(nèi)部改變變量的值,就要傳入指針,如果要在函數(shù)內(nèi)部改變指針的值,就需要傳入二級指針。
本文授權(quán)轉(zhuǎn)載自公眾號“wenzi嵌入式”,作者wenzid
-END-
推薦閱讀
免責(zé)聲明:本文內(nèi)容由21ic獲得授權(quán)后發(fā)布,版權(quán)歸原作者所有,本平臺僅提供信息存儲服務(wù)。文章僅代表作者個(gè)人觀點(diǎn),不代表本平臺立場,如有問題,請聯(lián)系我們,謝謝!