提到指針,我們都知道指針是用來存儲(chǔ)一個(gè)變量的地址。所以,當(dāng)我們定義了一個(gè)指向指針的指針的時(shí)候(pointer to pointer),我們也稱之為二級(jí)指針,那針對(duì)于這個(gè)二級(jí)指針來說,第一級(jí)指針存放的是指向的變量的地址,第二級(jí)指針存放的是第一級(jí)指針的地址。可以用下面這張圖表示他們之間的關(guān)系。
二級(jí)指針關(guān)系圖
上圖所表達(dá)的意思也就是,一級(jí)指針變量 ptr1 存放的是 var 變量的地址,二級(jí)指針變量 ptr2 存放的是一級(jí)指針變量的地址。這也就是關(guān)于二級(jí)指針的相關(guān)概念。
一級(jí)指針與二級(jí)指針關(guān)系示例
下圖是代碼運(yùn)行的結(jié)果:
代碼運(yùn)行結(jié)果截圖
結(jié)果也很明顯了,一級(jí)指針變量 p 存放的是變量 a 的地址,二級(jí)指針變量 q 存放的是一級(jí)指針變量 p 的地址,所以根據(jù)以上結(jié)果也能得出下面的等式:
q = &p;*q = p = &a;**q = *p = a;
在了解了上述一級(jí)指針和二級(jí)指針的一個(gè)關(guān)系之后,我們?cè)賮砜戳硗庖粋€(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)系是這樣的:
變量關(guān)系圖
根據(jù)上面這個(gè)圖我們也可以知道,對(duì)于 ipp 的兩次解引用的結(jié)果是 i 的值,也就是說 **ipp = 5,我想對(duì)于這個(gè)的理解并不困難,如果我繼續(xù)在這個(gè)基礎(chǔ)上添加代碼,注意,是在上條代碼的基礎(chǔ)上添加如下代碼:
*ipp = ip2;
在這條代碼的作用下,數(shù)據(jù)關(guān)系圖就發(fā)生了改變,改變?nèi)缦滤荆?/p>
數(shù)據(jù)關(guān)系圖
對(duì)于上述的變化來說,我們?cè)黾拥拇a改變的是 *ipp 的值,也就是說 ipp 的值是不會(huì)發(fā)生改變的,既然 ipp 的值不會(huì)發(fā)生改變,那么 ipp 指向 ip1 的關(guān)系不會(huì)發(fā)生改變,我們?cè)黾拥拇a改變了 *ipp 的值,那么也就是說改變了一級(jí)指針指向的值,而 ip2 是指向 j 的,所以也就有了上述的變化。
緊接著我們繼續(xù)在第一條增加的代碼的基礎(chǔ)上重新增加一條代碼,增加的代碼如下:
*ipp = &k;
那么這個(gè)時(shí)候所對(duì)應(yīng)的數(shù)據(jù)關(guān)系圖如下圖所示:
數(shù)據(jù)關(guān)系圖
這個(gè)原理和剛才的一樣,不在這里贅述了。
二級(jí)指針的應(yīng)用那再講述了上述的基本概念之后,我們知道二級(jí)指針變量是用于存放一級(jí)指針變量的地址的,那么在具體的實(shí)際應(yīng)用中,又在什么地方可以用到二級(jí)指針呢?下面來看一個(gè) C 語言函數(shù)傳址調(diào)用的例子。
我們?cè)趧倢W(xué)習(xí)指針的時(shí)候,都會(huì)碰到如下這樣一個(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ù)值,不會(huì)對(duì)實(shí)參本來的值發(fā)生改變。而將形參定義成了指針的話,那么傳到函數(shù)里面的值雖然是實(shí)參地址的一個(gè)副本,但是地址里存的值發(fā)生了改變,也就導(dǎo)致實(shí)參本來的值也發(fā)生了改變。
有了上述分析的基礎(chǔ)上,我們知道,如果要在一個(gè)函數(shù)內(nèi)改變一個(gè)數(shù)的值,那么就需要將形參定義為指針。同樣的,如果我們要在一個(gè)函數(shù)內(nèi)改變一個(gè)指針的值,我們就需要將形參定義了二級(jí)指針,下面來看這樣一個(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)槲覀円?allostr 里改變指針變量 copystr 的值(要使用 malloc 分配內(nèi)存),那么就需要把 copystr 的地址傳到函數(shù)里,那么這個(gè)時(shí)候,所定義的函數(shù)形參也就需要是二級(jí)指針了。
二級(jí)指針在單鏈表中的應(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)绻獎(jiǎng)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ǎng)h除的結(jié)點(diǎn)是第一個(gè)結(jié)點(diǎn)的時(shí)候,刪除結(jié)點(diǎn)示意圖如下所示:
第一個(gè)結(jié)點(diǎn)刪除原理
如果要?jiǎng)h除的結(jié)點(diǎn)不是處在第一個(gè)結(jié)點(diǎn)的位置,那么刪除結(jié)點(diǎn)的原理示意圖如下圖所示:
普通結(jié)點(diǎn)刪除
上述就是一個(gè)使用一級(jí)指針操作鏈表的一個(gè)簡(jiǎn)單地例子,自己在理解這個(gè)例子的時(shí)候,也存在幾個(gè)對(duì)我來說的難點(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í)是對(duì)于指針的理解,head 指向了第一個(gè)結(jié)點(diǎn),一定注意在這里的 head 是頭指針,并不是頭結(jié)點(diǎn)。(這是筆者個(gè)人的理解,如果大家有不同的看法,歡迎各位朋友添加筆者微信共同探討)。
第二個(gè)難點(diǎn)就是上述函數(shù)中,函數(shù)有一個(gè)返回值,返回了頭指針。為什么要返回呢?是因?yàn)楫?dāng)前傳入函數(shù)的形參是一級(jí)指針,在函數(shù)內(nèi)部改變 head ,在函數(shù)運(yùn)行結(jié)束時(shí),head 值并不會(huì)發(fā)生改變,所以要返回。
第三個(gè)難點(diǎn),那么為什么鏈表操作中,又能夠刪除中間的結(jié)點(diǎn)呢?是因?yàn)殡m然 傳進(jìn)去的 head 是一級(jí)指針,但是 head 結(jié)構(gòu)體成員內(nèi)的 next 是一個(gè)指針,那這樣的話,對(duì)于 next 成員來說它是一個(gè)二級(jí)指針,對(duì)于他的變化,在函數(shù)結(jié)束時(shí)是會(huì)產(chǎn)生改變的,所以可以刪除中間的結(jié)點(diǎn)。
二級(jí)指針在單鏈表結(jié)點(diǎn)刪除的應(yīng)用上面的例子中,在刪除單鏈表的結(jié)點(diǎn)的時(shí)候,我們形參采用的是一級(jí)指針的方式,在這個(gè)過程中,還需要引入 pre 指針來解決這個(gè)問題,還有一種很巧妙的方法,利用了二級(jí)指針的特性解決了結(jié)點(diǎn)刪除的問題,在這個(gè)過程中,運(yùn)用二級(jí)指針,不需要進(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í)指針,這樣子進(jìn)行遍歷在函數(shù)結(jié)束后不會(huì)改變其本身的鏈表結(jié)構(gòu)。然后,在進(jìn)行刪除的時(shí)候,(*head) 在函數(shù)結(jié)束后是會(huì)保持其在函數(shù)內(nèi)的變化值的,所以也就完成了結(jié)點(diǎn)的刪除。
評(píng)論