重新认识二级指针(Pointers to Pointers)
四年前(2010年),我寫了一篇關(guān)于我自己對于二級指針(Pointers to Pointers)的理解:《深入理解雙指針》。這篇文章在網(wǎng)上一直存在著很大的爭議,后面的評論也有很多質(zhì)疑的聲音。通過這幾年我對C/C++更加深入的理解,我覺得有必要重新寫一篇對于二級指針(雙指針)的理解。
另外,本章中使用的程序是使用Linux的GCC編譯出來的,所以匯編代碼使用的是AT&T匯編指令,跟windows下使用Intel指令有所不同,詳見AT&T與Intel匯編比較。同時(shí),由于我是用的是64位機(jī)器,為了方便講解32位的程序以及防止編譯器對代碼的優(yōu)化影響我們對問題的分析,本章所講解的所有代碼編譯選項(xiàng)為:gcc -m32 -O0。
概述
Pointers to Pointers:二級指針,我之前把它叫做雙指針,比較專業(yè)的叫法是二級指針。二級指針是相對一級指針而言的。
二級指針一般用于函數(shù)參數(shù)傳遞:
C語言參數(shù)值傳遞
很多C語言書上,對于參數(shù)的值傳遞都講解的不是很清楚。對于值傳遞的理解有助于理解我們理解二級指針。
普通變量的值傳遞
先看看一段代碼:
1 #include <unistd.h> 2 #include <stdio.h> 3 #include <stdlib.h> 4 5 void increase(int value) 6 { 7 value = value + 1; 8 } 9 10 int main(int argc, char** argv) 11 { 12 int count = 7; 13 increase(count); 14 printf("count = %d\n", count); 15 16 return 0; 17 }這段代碼對應(yīng)的匯編代碼如下:
080483e4 <increase>: 80483e4: 55 push %ebp 80483e5: 89 e5 mov %esp,%ebp 80483e7: 83 45 08 01 addl $0x1,0x8(%ebp) 80483eb: 5d pop %ebp 80483ec: c3 ret 080483ed <main>: 80483ed: 55 push %ebp 80483ee: 89 e5 mov %esp,%ebp 80483f0: 83 e4 f0 and $0xfffffff0,%esp 80483f3: 83 ec 20 sub $0x20,%esp 80483f6: c7 44 24 1c 07 00 00 movl $0x7,0x1c(%esp) 80483fd: 00 80483fe: 8b 44 24 1c mov 0x1c(%esp),%eax 8048402: 89 04 24 mov %eax,(%esp) 8048405: e8 da ff ff ff call 80483e4 <increase> //[...]這段代碼執(zhí)行的結(jié)果 count = 7。 我是用gdb調(diào)試,打印ESP和count的地址如下:
(gdb) p $esp $2 = (void *) 0xffffd2b0 (gdb) p &count $3 = (int *) 0xffffd2ccmain函數(shù)內(nèi)部的匯編如下:
sub $0x20,%esp #esp-0x20,棧向下生長0x20,用來存放局部變量 #在內(nèi)存單元esp + 0x1c處存放7. #即count,我上面打印的 $3 - #2 = 0x1c. movl $0x7,0x1c(%esp) mov 0x1c(%esp),%eax #將內(nèi)存單元0x1c即count變量的值copy到EAX寄存器中 mov %eax,(%esp) #copy count變量的內(nèi)容到當(dāng)前的ESP寄存器所指向的內(nèi)存單元 call 80483e4 <increase> #調(diào)用increase函數(shù)在我的機(jī)器上當(dāng)前運(yùn)行的ESP指針指向的內(nèi)存單元是0xffffd2b0,棧向下生長了0x20,則當(dāng)前棧楨(Stack Frame)的起始地址是0xffffd2b0到0xffffd2d0。count是局部變量,占用的是棧空間,上面gdb打印出來count的地址0xffffd2cc,正好落在main函數(shù)的棧楨內(nèi)。
有一點(diǎn)需要注意的是,在increase調(diào)用之前,count變量被copy了一份放在當(dāng)前ESP所指向內(nèi)存單元0xffffd2b0,這個(gè)count就是為了用來傳遞參數(shù)用的。
接下來看看increase的匯編代碼:
push %ebp #ebp壓棧,保護(hù)上一個(gè)棧楨 mov %esp,%ebp #保護(hù)ESP addl $0x1,0x8(%ebp) #將copy出來的那個(gè)count變量+1 pop %ebp retincrease的匯編代碼比較簡單,這里只需要解釋下addl $0x1,0x8(%ebp)。
由前面一句mov %esp,%ebp可以發(fā)現(xiàn),此時(shí)EBP其實(shí)是指向棧頂。調(diào)用increase之前ESP是0xffffd2b0,由于調(diào)用increase需要將下一條IP指令壓棧,則ESP = ESP - 0x04 = 0xffffd2ac。在進(jìn)入increase之后,又執(zhí)行了一句push %ebp,ESP = 0xffffd2ac - 0x04 = 0xffffd2a8。那么此時(shí)棧頂就是0xffffd2a8,EBP的內(nèi)容就是0xffffd2a8。0x8(%ebp)表示的是EBP + 0x8處的內(nèi)存單元:0xffffd2a8 + 8 = 0xffffd2b0出的內(nèi)存單元。
addl $0x1,0x8(%ebp)這句匯編就是在內(nèi)存單元0xffffd2b0處的內(nèi)容加+1,最終將加一后的結(jié)果繼續(xù)存放在0xffffd2b0處 。再回顧下,前面0xffffd2b0存放的內(nèi)容:沒錯(cuò),就是copy出來的count。
看到這里,你會(huì)發(fā)現(xiàn),在count傳遞到increase之后,一直都是在操作copy出來的那個(gè)count臨時(shí)變量,而沒有操作真正的count變量。可見,對于普通變量而言,參數(shù)的值傳遞就意味著只是簡單的將變量copy了一份傳遞給函數(shù),普通變量是無法改變外部原始變量的值。
指針的值傳遞(一級指針)
還是先看代碼:
1 #include <unistd.h> 2 #include <stdio.h> 3 #include <stdlib.h> 4 5 void increase(int* ptr) 6 { 7 *ptr = *ptr + 1; 8 } 9 10 int main(int argc, char** argv) 11 { 12 int count = 7; 13 increase(&count); 14 printf("count = %d\n", count); 15 return 0; 16 }這段代碼對應(yīng)的匯編代碼如下:
080483e4 <increase>: 80483e4: 55 push %ebp 80483e5: 89 e5 mov %esp,%ebp 80483e7: 8b 45 08 mov 0x8(%ebp),%eax 80483ea: 8b 00 mov (%eax),%eax 80483ec: 8d 50 01 lea 0x1(%eax),%edx 80483ef: 8b 45 08 mov 0x8(%ebp),%eax 80483f2: 89 10 mov %edx,(%eax) 80483f4: 5d pop %ebp 80483f5: c3 ret080483f6 <main>: 80483f6: 55 push %ebp 80483f7: 89 e5 mov %esp,%ebp 80483f9: 83 e4 f0 and $0xfffffff0,%esp 80483fc: 83 ec 20 sub $0x20,%esp 80483ff: c7 44 24 1c 07 00 00 movl $0x7,0x1c(%esp) 8048406: 00 8048407: 8d 44 24 1c lea 0x1c(%esp),%eax 804840b: 89 04 24 mov %eax,(%esp) 804840e: e8 d1 ff ff ff call 80483e4 <increase> // [...]這段代碼的執(zhí)行結(jié)果是8。
這段代碼跟上一段代碼的唯一區(qū)別是將count的地址傳遞給increase函數(shù)了。
main函數(shù)的匯編代碼
push %ebp mov %esp,%ebp and $0xfffffff0,%esp sub $0x20,%esp movl $0x7,0x1c(%esp)lea 0x1c(%esp),%eax #將count變量的地址賦值給EAX mov %eax,(%esp) call 80483e4 <increase>跟前面的main函數(shù)的唯一區(qū)別是lea 0x1c(%esp),%eax
看懂這段代碼首先要補(bǔ)習(xí)下lea指令。lea指令跟mov指令很相似,區(qū)別在于lea類似于C語言中的&取地址。那么lea操作也只是簡單的針對地址做加法而已,而不會(huì)針對這個(gè)地址單元取操作數(shù)。
那么這代碼在調(diào)用increase函數(shù)之前,當(dāng)前ESP所指向的內(nèi)存單元的值是count變量的地址。而上一段代碼在調(diào)用increase之前,當(dāng)前ESP所指向的內(nèi)存單元的值是count臨時(shí)變量的值。
我們再來看看increase函數(shù)的匯編代碼
push %ebp mov %esp,%ebp mov 0x8(%ebp),%eax #前面已經(jīng)講過了 # 取出EAX所指向的內(nèi)存單元的值賦值給EAX # 也就是說執(zhí)行此句話之后,EAX的內(nèi)容是 # count變量的值,而不是地址。 mov (%eax),%eax lea 0x1(%eax),%edx #將EAX的內(nèi)容加一,將加一后的結(jié)果存放到EDX mov 0x8(%ebp),%eax #重新將count變量的地址賦值給EAX #將EDX的內(nèi)容存放到EAX所指向的內(nèi)存單元 #就是將加一后的結(jié)果重新賦值給main函數(shù)里的count變量 mov %edx,(%eax) pop %ebp ret理解這段匯編代碼,需要記住一點(diǎn),在調(diào)用increase之前,棧頂ESP所指向的內(nèi)存單元的值是count變量的地址。之后,經(jīng)過壓棧IP,進(jìn)入increase函數(shù),再壓棧EBP。則0x8(%ebp),EBP + 0x8表示的就是在調(diào)用increase前,棧頂所指向的內(nèi)存單元,里面存放的是count變量的地址。也就是說mov 0x8(%ebp),%eax之后,EAX的內(nèi)容就是count變量的地址。緊接著mov (%eax),%eax是現(xiàn)將EAX指向的內(nèi)存單元的內(nèi)容取出來存放到EAX中,此時(shí)EAX寄存器的內(nèi)容已經(jīng)不是地址了,而直接是count變量的值。然后對其做加一操作,存放到EDX當(dāng)中。
下面是最關(guān)鍵的兩句話:
mov 0x8(%ebp),%eax mov %edx,(%eax)由于EBP + 0x8里面放的是count變量的地址,mov 0x8(%ebp),%eax之后,EAX中存放的就是count變量的地址。
EDX存放的是前面計(jì)算的結(jié)果,最后mov %edx,(%eax),將前面計(jì)算的結(jié)果重新存放到EAX所指向的內(nèi)存單元,即重新給count變量賦值。
看到這里,你會(huì)發(fā)現(xiàn),函數(shù)參數(shù)值傳遞,對于指針變量來說,也只是僅僅傳遞了一個(gè)內(nèi)存地址,然后對這個(gè)內(nèi)存地址進(jìn)行操作。由于內(nèi)存地址是進(jìn)程級別的,所以,在函數(shù)內(nèi)部 ,對地址所指向內(nèi)容的修改,是可以帶到函數(shù)外部的,是可以操作到函數(shù)外面的源變量的。
二級指針
我們改造下上面的代碼
1 #include <unistd.h> 2 #include <stdio.h> 3 #include <stdlib.h> 4 void increase(int* ptr) 5 { 6 *ptr = *ptr + 1; 7 ptr = NULL; 8 } 9 10 int main(int argc, char** argv) 11 { 12 int count = 7; 13 int* countPtr = &count; 14 increase(countPtr); 15 printf("count = %d\n", count); 16 printf("countPtr = %p\n", countPtr); 17 return 0; 18 }運(yùn)行結(jié)果,count = 8,而countPtr則不是NULL。
運(yùn)用前面的理論,其實(shí)很容易分析出問題。一級指針變量,也是一個(gè)普通變量,只不過這變量的值是一個(gè)內(nèi)存單元的地址而已。countPtr在傳遞給increase之前,被copy到一個(gè)臨時(shí)變量中,這個(gè)臨時(shí)變量的值是一個(gè)地址,可以改變這個(gè)地址所在內(nèi)存單元的值,但是無法改變外部的countPtr。
從這個(gè)結(jié)果可以得出一個(gè)結(jié)論:一級指針作為參數(shù)傳遞,可以改變外部變量的值,即一級指針?biāo)赶虻膬?nèi)容,但是卻無法改變指針本身(如countPtr)。
有了上面的理解基礎(chǔ),其實(shí)對于理解二級指針已經(jīng)很容易了。
對于指針操作,有兩個(gè)概念:
- 引用:對應(yīng)于C語言中的&取地址操作
Reference
- 解引用:在C語言中,對應(yīng)于->操作。
Dereference operator
對于一個(gè)普通變量,引用操作,得到的是一級指針。一級指針傳遞到函數(shù)內(nèi)部,雖然這個(gè)一級指針的值會(huì)copy一份到臨時(shí)變量,但是這個(gè)臨時(shí)變量的內(nèi)容是一個(gè)指針,通過->解引用一個(gè)地址可以修改該地址所指向的內(nèi)存單元的值。
對于一個(gè)一級指針,引用操作,得到一個(gè)二級指針。相反,對于一個(gè)二級指針解引用得到一級指針,對于一個(gè)一級指針解引用得到原始變量。一級指針和二級指針的值都是指向一個(gè)內(nèi)存單元,一級指針指向的內(nèi)存單元存放的是源變量的值,二級指針指向的內(nèi)存單元存放的是一級指針的地址。
二級指針一般用在需要修改函數(shù)外部指針的情況。因?yàn)楹瘮?shù)外部的指針變量,只有通過二級指針解引用得到外部指針變量在內(nèi)存單元的地址,修改這個(gè)地址所指向的內(nèi)容即可。
我們針對上面的代碼繼續(xù)做修改
1 #include <unistd.h> 2 #include <stdio.h> 3 #include <stdlib.h> 4 void increase(int** ptr) 5 { 6 **ptr = **ptr + 1; 7 *ptr = NULL; 8 } 9 10 int main(int argc, char** argv) 11 { 12 int count = 7; 13 int* countPtr = &count; 14 increase(&countPtr); 15 16 printf("count = %d\n", count); 17 printf("countPtr = %p\n", countPtr); 18 return 0; 19 }這段代碼,運(yùn)行結(jié)果count = 8, countPtr = NULL;
總結(jié)
首先,指針變量,它也是一個(gè)變量,在內(nèi)存單元中也要占用內(nèi)存空間。一級指針變量指向的內(nèi)容是普通變量的值,二級指針變量指向的內(nèi)容是一級指針變量的地址。
總結(jié)
以上是生活随笔為你收集整理的重新认识二级指针(Pointers to Pointers)的全部內(nèi)容,希望文章能夠幫你解決所遇到的問題。
- 上一篇: c语言利用文件体写在桌面上,在C语言中怎
- 下一篇: 关于“指针的指针”的认识(值传递、指针传