深入理解C++的指針和引用


每位學習過C++的程序員都知道指針其實就是地址(這樣理解是不完全正確的,但也無可厚非,為了方便暫且這么理解着吧)。在C++中引用的內在表現其實也是地址,但引用和指針的外在表現是不同的。也就是說從匯編語言級和機器語言級來理解,在C++中指針和引用是完全等價的,沒有任何的區別。但是反應到C++程序語言的語法上,指針和引用是兩個完全不同的概念。之所會如此完全是因為C++編譯器程序“搞的鬼”。接下來就讓我們一起學習,一層一層的揭開C++中指針和引用的面紗,看看他們的本質到底是什么。

下面我首先來做一個錯誤的論述,因為這個論述可能已經深入人心了。並且可能是從我們剛學習C++語法時就被灌輸了這樣的思想。但這種對C++指針和引用的理解只能說是理解了C++中指針和引用的表現形式,而沒有真正明白其在計算機內存中的本質。好了,費話有點多,下面進入正題。

在此先鄭重聲明一下,在接下來的論述中,凡是包含在“{}”中的全是指針和引用在C++語法中的表現形態,而非其在計算機內存中的本質。當我們學習到指針和引用在計算機內存中的本質時,你會發現這些論述可能是錯誤的。

//注意有花括號

因為指針作為地址指向的是一個用來存儲地址的內存空間,即它所指向的內存中存儲了“它指向的目標類型變量”的地址(這句話如果用英語定語從句表達就比較好理解了);當要訪問指針指向的內容時,需要先通過指針取出其中存儲的目標地址,再通過取出來的目標地址去找到我們要訪問的內存,從而達到我們的訪存目的,這是一個間接尋址的過程,並且屬於“二次訪存的”間接尋址。而引用沒有指針那么復雜,它作為地址指向的是存儲目標類型變量的內存空間,可以直接使用它訪問目標內存空間;屬於直接尋址。

//花括號結束

//雖然下面的比喻是我們學習C++的啟蒙,但也改變不了它是錯誤論述的命運

做個比喻,把地址比作鑰匙,把內存空間比作抽屜(在C++經典教材中經常做這種比喻)。一個地址對應一段內存空間,也就是一把鑰匙對應一個抽屜。對於引用來說,這把鑰匙你是直接拿在手里的,可以直接使用去那個抽屜里取你需要的東西。而對於指針來說,這把鑰匙是被鎖在一個抽屜里的,你是手中拿着一把可以打開這個抽屜的鑰匙(用英語中的定語從句理解更容易懂)。

引用,開一次抽屜就能達到目的,很直接。指針則要開兩次抽屜,所以是間接的達到目的。

//當知道這個比喻不正確的時候,我在感性思想上也有些接受不了

//雖然現在知道了這論述是錯誤的,但我曾經證明過它是對的

如果你對C++程序進行調試,並在Watch窗口中查看指針變量和引用變量的信息,通過分析總結就能夠完全理解C++中指針和引用的內涵了。

如果真的這樣做了你還會發現一些有趣的東西。凡是玩過匯編語言的都應該知道,我們所聲明的變量,在程序段中都是以一個32位的整數表示,其實這就是一個內存空間的地址(對變量的訪問屬於直接尋址)。對所有變量求地址后,仔細查看Watch窗口中的信息,你會發現,你聲明的“引用”所對應的地址和你這“引用”所引用對象所對應的地址是完全一樣的。地址一樣指向的存儲空間就是同一個內存空間,也就是同一個變量了。再看看指針,你會發現指針對應的地址,和指針所指向變量名所對應的地址是不同的。那么它們的聯系在哪兒呢?直接在Watch中查看指針名(得到的Value是指針變量所對應地址的內存中存儲的內容,也就是指針指向的內存地址),你會發現指針中存放的是一個32位的地址,而這個地址正是指針所指向的變量所對應的地址。

空口大白話也沒什么說服力,下面我來寫段代碼調試一下,有圖有真相。

VC6.0中編寫代碼如圖(1)所示

圖片

圖(1

從圖中可以看到,我們在程序結尾處設置了斷點。這樣做的目的是讓程序運行完,所有的變量都有值。如果程序沒有運行到最后,那么在一些語句執行之前相關的變量是沒有被分配存儲空間的。

Watch的內容如圖(2)所示

圖片

圖(2

觀察全局變量a,b,c;從圖(2)中可以看到對變量a和變量b求地址,得到的值是相同的,也就是說變量a和變量b對應的是同一內存空間。也即他們是同一個變量,只是在C++中的名字不同而已。而指針變量c的地址不是我們關心的,我們關心的是指針變量c中所存儲的內容,這內容正是變量a和變量b共同的地址。

再看局部變量aa,bb,cc,從圖(2)中可以看到它們之間的關系與變量a,b,c之間的關系相同。這樣也就驗證了上面的論述。

//其實以上“{}”中的內容不能算是錯,只能說是在C++的語法范疇內適用,出了C++就不完全正解了。

//但接下來這個結論中的幾句話,就需要特別關注了,尤其是紅色字體那一句。

通過以上分析可以得出結論:變量和引用是一樣的,引用只是變量的一個別名,其對應的內存空間與變量相同,是同一個變量。用引用訪問內存時與變量相同都是直接尋址。指針與引用不同,指針中存儲的是變量的地址。使用指針訪問內存時,需要先從指針變量中取出變量的地址,然后根據變量地址訪問變量對應的內存空間,屬於(二次訪存)間接尋址。

//現在想想當初證明的是一個錯誤觀點,而且還是有理有據,真的很感慨。但我們都是一路從錯誤中走來,最終找到了正確的方向

看到上面這么多的花括號,是不是很不服氣?里面的論述有理有據,憑什么說人家是錯誤的呢?證明上面這些花括號里的內容錯誤,還真不是件容易的事,還是別花時間費精力的證明它錯了。接下來我就把另外一種論述分享給大家,並且證明接下來的論述才是正確的。

請仔細聽好下面這句話:其實在C++中引用和指針在內存中的表現是完全相同的,並且在匯編語言級和機器語言級上完全一至,沒有任何區別。它們都是用來存儲其它類型變量地址的內存塊,並且都是(二次訪存)間接尋址。

看到這句話,你會不會很驚訝?但它確確實實是正解的。

接下來還是有圖有真相,開始分析。

程序的代碼還是如圖(1)所示的VC6.0環境下的代碼,同樣是編譯組建后進行調試。進入調試之后,我們查看C++代碼編譯后的匯編語言代碼來分析。VC6.0對圖(1)中的C++代碼編譯成的匯編代碼如圖(3)和圖(4)所示(這里只截取了需要分析的主要代碼)。

圖片

圖(3

 

從圖(3)和圖(4)中可以看到,變量的聲明部分是沒有生成可執行的匯編代碼的(在數據庫段定義部分肯定要有分配內在的代碼了)。而分析引用變量聲明並賦值的代碼可知,引用是被重新分配了存儲空間的,與引用對應的內存中存儲的是所引用變量的地址(這好像是指針的特性吧?哈哈,往下會更有意思)。可見僅簡單的分析一下生成的匯編代碼就能知道引用並不像本文章中最開始論述的那樣。

下面集中精力看一下圖(4)中的局部變量,局部變量aa聲明並初始化,是在stack中分配了存儲空間,並把初始值存到了stack中。往下看可以發現變量aa的引用和指針是完全一樣的。都是在stack中分配內存並存儲變量aa的地址。

圖片

圖(4

再看看全局變量a,b,c的賦值操作的代碼,可以發現,對引用b和指針c的操作完全相同。那對於局部變量呢,比較一下引用bb和指針cc看一下。很明顯對他們的操作也完全相同。

通過以上分析可以得出,在C++中引用和指針在匯編語言級是完全相同的,都是以(二次訪存間接尋址方式對數據進行操作的。

雖然得到了這樣的結論,但這樣的結論有什么用呢?我們用C++編程,就是為了避免像匯編語言那種復雜性。而在C++封裝后的特性中,引用和指針是完全不同的兩個概念。而封裝后的引用看起來真的就像和變量一樣,可以像使用變量一樣使用它。我們完全不用去關心他在匯編語言級的本質,就更不要提機器語言了。

是的,如果不考慮代碼的效率問題,的確可以不去關心引用和指針在匯編語言級和機器語言級的本質。如果考慮到代碼的執行效率時,就不得不去深入探討它們在低級語言中的本質了。如果按C++中對引用特性的理解,對它的操作應該歸到直接尋址里面,但實際上它卻是要二次訪存的間接尋址。對於數據的訪問速率大家都應該知道,內存快於外存,緩存快於內存,寄存器快於內存。在尋址的時候,直尋址要比間接尋址快,在間接尋址中,訪存次數越少速率越快。那么你把引用當成直接尋址,他就要比指針訪問的間接尋址速率要快。但實際上,它們的速率是相同的,因為在機器語言級,實現他們的機器指令是一樣的,都是二次訪存的間接尋址。

至此,C++用的引用和指針的特性已經討論完畢了,本文不僅對於在C++中如何理解引用和指針進行了詳細的說明,還在匯編語言級對二者的本質進行了論述。至於如何靈活巧妙的使用它們,就需要你在編寫代碼的過程不斷的熏陶了,閱讀別人的東西是無法真正掌握它們的使用技巧的。

    最后說句題外話,其實 Java 中的對象引用和 C++ 中的引用在使用上是完全相同的(內部實現可能不同,但完全可以當成一樣東西來理解,因為外在表現是完全相同的)。關於這部分的分析和論述,可以參閱我將來要寫的一篇《 Java C++ 互譯——“對象引用”的解決方案》。 


注意!

本站转载的文章为个人学习借鉴使用,本站对版权不负任何法律责任。如果侵犯了您的隐私权益,请联系我们删除。



 
粤ICP备14056181号  © 2014-2021 ITdaan.com