1. 程式人生 > >從彙編和高階語言的角度理解傳值方式,傳值,傳引用,傳指標的本質機制與區別。白話通俗易懂。

從彙編和高階語言的角度理解傳值方式,傳值,傳引用,傳指標的本質機制與區別。白話通俗易懂。

函式的傳參與返回值的方式有傳值和傳遞引用,c語言中就是傳值,而c++擴充套件傳引用。

而傳值分為傳遞值(實參的值,此時形參是實參在記憶體中的一份拷貝,形參在使用時分配記憶體,結束時釋放,實參和形參在記憶體中的地址不同,因此對形參的改變不會改變實參)

傳值的另外一種是傳指標(傳遞的值是實參的地址,此時形參的值為實參的地址,所以對形參的修改就會修改實參的值)

而傳引用,從高階語言的層面看,引用是實參的別名,所以對形參的修改就是直接對實參的改變。引用是變數的地址,可以理解為一個常指標,也就是指向固定的指標,但是與指標還是有區別的,具體使用的時候不同,並且引用於指標有很多不同,引用必須初始化,但是指標可以定義的時候不初始化,另外指標可以指向空NULL,這一方面引用比指標更安全,但是引用還是存在風險的只是風險小一點而已。比如,引用一個不合法的地址。舉例:

int *e=newint(10);
int &f=*e;

delete e;
f=30;

從彙編底層程式碼的層面看,如果將函式傳參的傳指標和傳引用的方式的函式分別生成彙編程式碼發現,是一模一樣的,主調函式的傳參的彙編程式碼也是一模一樣的,所以從底層彙編程式碼看,傳引用就是通過傳指標實現的。彙編程式碼都是通過lea取地址和mov把取到的地址放到一塊記憶體中。


還有一個問題是,引用的本質是什麼,引用到底佔不佔記憶體?

主要是針對很多關於引用的說法上,比如引用是別名,不是值不佔記憶體,只有宣告沒有定義。

其實這些說法是不準確的,沒有從深層上去分析,只是表面上看到的現象說法。確切說是編譯器給你做的優化的表象。因為從底層彙編實現程式碼上看,引用和指標完全一樣,根本的區別就在編譯器處理的環節,很多限定都是在編譯時限制的,比如引用是指標指向固定的指標,這個定向就是在編譯環節限定的,像const和private等一樣,在彙編程式碼中並沒有相應的程式碼,都是在編譯器處理的時候做相關的限定的。

int a =10;

int *p=&a;

int &q=a;

指標p,每次都要先到指標p自身的地址把p的內容(p的內容是p指向的內容的地址&a)取到,然後再到取到的地址&a中把內容10取出來,也就是先定址後取值。而編譯器對引用q做了優化,每次碰到引用q就相當於*p,不用先去p裡面取地址,直接就是相當於a,對所有q的操作都相於是對*p的操作也就是對a的操作,所以對q取地址,就是對a取地址,也就是&q=&a。(所以當利用sizeof對指標p,是指標本身的所佔位元組數,也就是地址&a的存放的位元組數,而sizeofq的時候,因為q相當於a或者相當於*p,就是10所佔的位元組數。也就是說所有對實際引用q的操作都會被編譯器轉化為a的操作,不會得到實際的q的操作;另外指標和引用的自增++操作得到的結果也不同,指標自增p++,指向變化了,而引用的++相當於(*p)++,相當於a++。這個是是說明了指標本身的值(所指向的地址)是以傳值的方式傳遞的,改變本身的值(地址值)只會改變指標本身的指向,不會改變指標所指向的變數的值,而傳引用,引用本身是通過傳遞引用的方式傳遞的,改變引用本身的值就是改變所對應的變數的值。)eg:
void func(int* p, int&r); 


int a = 1; 
int b = 1; 
func(&a,b); 
指標本身的值(地址值)是以pass by value進行的,你能改變地址值,但這並不會改變指標所指向的變數的值, 
p = someotherpointer; //a is still 1 

但能用指標來改變指標所指向的變數的值, 
*p = 123131; // a now is 123131 

但引用本身是以pass byreference進行的,改變其值即改變引用所對應的變數的值 
r = 1231; // b now is 1231

所以也就是看上去引用q就是a的一個別名(綽號),比如一個人有一個大名,一個小名,小名就是這個人的一個別名,你叫他的小名說他怎麼樣怎麼樣,其實就是在說這個人怎麼樣怎麼樣一個意思。還有取地址的時候發現得到的也是a的地址,所以感覺應用是不佔用記憶體的。這裡10是放在記憶體中開闢的一個int型大小的記憶體空間中,而變數名a實際是不存在的,對應於底層實現,編譯器生成的目標檔案中它會用存放10的記憶體地址代替所有的變數名a,所以變數名a不佔用記憶體。可以看出變數的實質是編譯器的一種抽象,一種底層實現機制的透明。變數名實質是記憶體一塊區域的地址,改變這個變數就是改變了這塊記憶體地址裡面的內容,編譯器抽象後讓我們可以不必瞭解這些底層實現細節,只是簡單的看到有一個變數a,它的值是10,我們可以改變a的值這樣。在此處引用的變數名q相當於a的一個別名,同理在編譯器優化下,生成目標檔案中也會用存放10的記憶體地址來代替所有的引用的變數名q,可以認為引用的變數名q不佔用記憶體。但是從實際的彙編程式碼的底層實現看,引用還是會佔記憶體的,用來存放10也就是a這個地址的。一般是4位元組大小,只是每次編譯器都優化導致我們取不到引用的地址而已。Eg:

#include<stdio.h>

int main()

{

    int x=1;

    int y=2;

    int &b=x;

    printf("&x=%x,&y=%x,&b=%x,b=%x\n",&x,&y,&y-1,*(&y-1));

    getchar();

    return 0;

 }

上面就證明了本身引用是佔記憶體的,下面來看編譯的優化,&b=&(*b)=&a

#include<stdio.h>

int main()

{

    int x=1;

    int &b=x;

    printf("&x=%x,,&b=%x\n",&x,&b);

    getchar();

    return 0;

 }



所以引用在底層彙編程式碼與指標相同,底層程式碼也是通過指標實現的,可以看成是一個定向的常指標,但是編譯器階段使用是不同的,他相當於*p,相當於直接對它引用的變數的操作,但是當作引數傳遞的時候,它又區別於變數a,因為傳引用,形參的改變可以改變實參,而普通的傳值,傳變數a的值時,形參的改變是不可以改變實參的值的,這是因為傳引用時,傳的是地址,是變數名a,形參和實參都具有相同的地址。而普通傳值時傳遞的是a裡面的內容10這個值,傳進去以後形參單獨放在一個地址來儲存這個10,也就是形參和實參不具有相同的地址。另外,引用在定義時必須進行初始化,不可以引用NULL,一旦被初始化,就不可以變化了,從一而終,而指標可以變化,可以指向NULL,指標可以指向別的地方等。

什麼時候在指標和引用之間使用引用的情況?

根據more effective c++之中介紹的總結一下就是:

在必須要指向一個指向並且不想改變這個指向時,以及在過載操作符並且為了防止不必要的語義誤解時,選用引用,其他時候選用指標。

在可能存在不指向任何指向的情況或者會改變指向的情況的時候,不應該是使用引用。

一般預設程式設計師不會犯不初始化,或者對不合法區域的引用時,預設使用引用比指標更加安全高效,因為預設都已經初始化而且合法的,而指標可以先定義以後在初始化,所以指標使用前需要檢查,防止出現野指標的情況。

string&rs; // 錯誤,引用必須被初始化
string s("xyzzy"); 

string& rs = s; // 正確,rs指向s 指標沒有這樣的限制。 
string *ps; //
未初始化的指標 
//
合法但危險 

另外還是在另外一篇博主(http://blog.csdn.net/whzhaochao/article/details/12891329)只是從高階語言層面分析了幾種傳值方式的比較,比較清晰明瞭,如果結合上面我們的這些知識分析來看下面我轉過來的(只是轉帖的圖)(由於原博主畫的圖比較好,我就沒有再單獨畫了,而且根據上面底層的理解再來從高階語言看,非常簡單易懂)

我把博主裡面的內容用更加通俗的白話用自己的話來分析了一下,只是單獨執行一下程式的話,記憶體值變化,博主那幾個圖畫的很好,我就直接拿過來了,不然畫箭頭太麻煩了。

1傳值

這個在形參和實參那個文章中已經分析過了,從彙編底層來說,它們存在各自的函式棧之中,存在不同的記憶體之中,地址不同,所以改變形參不會改變實參。這個時候的值的形參就是是實參的一份拷貝。


從右邊的執行結果,我們可以看出來a和p的地址不同,改變形參p沒有改變實參a。


可以看出形參p是對實參a在記憶體中另外一個地方的一個拷貝。其實實參a在主調函式的棧空間中分配的記憶體中,而形參p在被調函式中的棧空間中分配的記憶體中,當被調函式結束時,p會被釋放掉。

2傳引用:


右邊的結果可以看出來,a與p的地址是相同的,改變形參p就是對實參a的改變,大家可以結合上面我們的分析具體來理解一下。


對p的操作就是直接等價於對a的操作,p編譯器用*p操作代替,也就是變數名a,0x10的地址。前面已經有非常詳細的分析了,不再贅述。不同於指標還要先定址在取值,可以參照對比接下來的傳指標的圖來對比理解。

3傳指標


這個時候a和p的地址是不同,因為p有自己的本身的地址,(其實引用也有自己的地址,我們在前面講解的時候已經用程式分析過了,由於編譯器的原因我們用&p是取不到引用自身的地址,而是&a),傳指標,也是可以達到改變形參來改變實參,只是要先定址再取值的操作。


4指標的引用傳遞,這個大家可以簡單理解一下,不過通過前面的,對這個理解也是非常容易的。


在這裡是新定義了一個指標b,然後把b這個指標的引用傳遞,指標是有自己的記憶體地址的,所以&b不等於&a,同時b傳遞的是引用所以&b=&p,p是b的一個別名,b是指向a的,所以裡面的內容都是&a,而且這種方式同理也是可以改變形參也是會相應的改變實參的值的。


再看不用指標的引用來傳遞的情況


這個時候,因為b是傳遞的指標,所以p有自己的本身的地址了,所以&p不等於&b了,但是&p和&b都是指向a的。其實這個時候p相當於一個指向指標的指標。


一個由傳指標帶來的錯誤案例:


這個是由於allocate函式傳遞的是指標,所以p本身有自己的地址,&p不等於&str,p指向str,裡面的內容是str的內容為0,但是p呼叫malloc分配記憶體單元的時候,是給p分配的,由於p和str不是直接等同的,記憶體地址不同,所以沒有達到通過形參分配記憶體達到給實參分配記憶體的目的,導致strcpy的出錯。

正確的,也就是是p和str直接等同為同一塊記憶體單元,那麼就是引用,通過別名的形式。


是不是通過我們前面底層的分析,在來從高階語言的角度能夠更加抓住本質。