2020-12-6(從反彙編理解指標和引用的區別)

尋夢之璐發表於2020-12-10

這是我10個月前看到的一篇部落格吧,感覺分析指標和引用的文章這是我目前見過講解得最清晰的一篇:

本文主要基於反彙編程式碼,從初始化、賦值以及取地址三個角度來理解指標和引用的區別。
初始化
寫出以下程式碼並檢視反彙編程式碼:

int main()
{
	int x = 5;
        int * ptr = &x;  //指標
	int & ref = x;   //引用
 
        return 0;
}

在這裡插入圖片描述
在初始化階段,指標和引用的行為都是一樣的:先將x的地址載入到暫存器eax中,然後把eax的值拷貝到另一個記憶體地址中。

從反彙編中可以看到,x的地址就是ebp-0ch,對於指標來說,用x的地址來初始化指標ptr實際上就是把x的地址ebp-0c放到了ptr的地址單元中,而ptr的地址則是ebp-18h;

對於引用來說,這裡就有點問題了:常說的引用都是“變數的別名”,似乎ref和x就應該是同一個地址,而實際上這裡做了和指標初始化相同的操作——把x的地址放到了另一個記憶體單元(地址為ebp-24h)中。其實在這就可以有一種猜測:雖然ref是x的引用,但是ref也有自己的地址的,而在初始化階段,它的地址單元中存放的是它所引用的變數x的地址。如下所示:
在這裡插入圖片描述

為了便於敘述,下文中把ref叫做“引用變數”,來表明它也是一個有地址的“變數”(這種說法並不準確,只是暫時找不到其它說法)。
賦值
由於自加自減也是一個賦值的過程,為了便於敘述,這裡就用自加來進行分析。寫出以下程式碼並檢視反彙編程式碼:

int main()
{
	......
        ptr++;   //指標自加
        ref++;   //引用自加
 
        return 0;
}

在這裡插入圖片描述
從反彙編程式碼可以看到,指標的自加和引用的自加是不同的,引用的步驟更多。

在指標方面,一共有三步:①把指標變數ptr記憶體單元中的資料拷貝到eax;②eax加4(這裡的4對應32位編譯器下int型的size);③將eax的值寫回到指標變數ptr的記憶體單元中。這三個步驟其實就做了一件事:把指標變數ptr記憶體單元中的資料加4。而在此之前指標變數ptr的記憶體單元中存放的是變數x的地址,因此,這就相當於斷開了ptr和x之間的聯絡;

在引用方面,一共有五步:①把引用變數ref記憶體單元中的資料拷貝到eax;②根據eax的值找到相應的記憶體單元並把該記憶體單元中的資料拷貝到ecx中;③ecx加1;④再次把引用變數ref記憶體單元中的資料拷貝到edx中;⑤把ecx的值寫到edx的值對應的記憶體單元中。要分析這五步做了什麼,一定要知道在此之前,引用變數ref的記憶體單元中存的是變數x的地址。這五步做的事就是:根據ref存放的x的地址來找到x的記憶體單元,然後把x的記憶體單元中的值加1。從這個過程可以知道,ref++;表面上是對ref進行自加,而實際上,ref只是一個媒介,通過ref找到x,然後對x進行自加。

通過對二者賦值,可以發現,引用變數ref的確有自己的地址,它在記憶體中是佔空間的。並且指標變數ptr完全具有“主導權”,對指標變數ptr進行賦值,改變的就是它本身;而引用變數ref則完全沒有“主導權”,對引用變數ref進行賦值,改變的並不是它本身,而是通過它所找到的x。如下所示:
在這裡插入圖片描述
取地址
對ptr和ref分別進行取地址,這裡的變數t和t1可以不用管它,只需要觀察取地址的過程。檢視相應反彙編程式碼:

int main()
{
	......
	//取地址
	int ** t1 = &ptr;
	int * t = &ref;
 
    return 0;
}

在這裡插入圖片描述
由於不需要管t和t1,因此只需要關注ptr和ref取地址各自反彙編程式碼第一行即可。

在指標方面,取出ptr的地址&ptr是通過lea eax,[ebp-18h]來實現的。這句彙編程式碼的意思是,把ebp-18h這個地址,載入到eax中。而在ebp-18h就是指標變數ptr的記憶體地址,因此,指標變數ptr取地址很直接,就是取出ptr的記憶體地址;

再看引用方面,取出ref的地址&ref是通過mov eax,dword ptr [ebp-24h]實現的。這句彙編程式碼的意思是,取出ebp-24h這個地址的記憶體單元中的值,拷貝到eax中。根據前面已經知道,ebp-24h是ref的地址,在這個地址中存放的是ref引用的x的地址,因此&ref實際上是x的地址

由此可以發現,對指標取地址,取出的就是指標變數的地址,而對引用取地址,取出的實際上是引用變數所在記憶體單元中的值,也就是它引用的變數的地址

總結

1.引用和指標都是佔用記憶體的。引用變數和指標變數各自的記憶體單元中,存放的都是它們引用或指向的變數的地址;

2.引用實際上是把引用變數本身給隱藏了,表面上對引用變數進行賦值,實際上改變的是它引用的變數的值;表面上是對引用變數取地址,實際上取出來的是它引用的變數的地址;

3.由於引用變數的操作的實際物件都是引用變數所在記憶體單元中的值,因此引用變數必須和另一個變數關聯起來。也就是說,引用必須初始化。原因很簡單,如果你不對一個引用進行初始化,那麼引用變數的記憶體單元中存放的都是垃圾資料,後面對引用變數進行操作時就會通過這些垃圾資料找到對應的記憶體地址,這是非常危險的;

那麼直接初始化的時候對引用賦一個常量值呢?這實際上也是不對的,因為引用的初始化實際是用用來初始化的變數的地址來初始化引用變數它自己的記憶體單元,而一個常量值哪有地址呢?雖然在C++11中已經可以用一個常量來初始化右值引用,但是通過反彙編可以看到,右值引用的根本,還是先用一個地址去儲存這個常量值,然後再用這個地址去初始化引用變數。

而指標變數則完全不一樣了,指標變數的操作的實際物件都是它自己,因此指標變數不像引用那樣必須初始化,但是為了安全,也應該初始化。

4.依然是由於引用變數的一切操作實際物件都是它引用的變數,因此從使用者角度來說是沒有入口讓使用者去修改引用變數本身的。換句話說,使用者層面沒有任何辦法去改變引用變數記憶體單元中存放的地址,這也就是為什麼引用一旦初始化後就無法再修改。

5.由於引用變數自身對於使用者是不可見的,對引用變數取地址得到的也不是引用變數的地址,因此你無法讓一個引用變數的記憶體中存放另一個引用變數的地址,換句話說,不存在引用的引用。而相反,由於指標變數取出來的地址就是它本身的地址,因此你完全可以把一個指標變數的地址存放在另一個指標變數的記憶體中,這也就是為什麼可以存在多級指標,但是多級引用是不允許的。

6.關於“引用是變數的別名”的考慮。感覺這句話既對也不對,說它不對是因為引用變數和引用的變數二者實際上是獨立的兩塊記憶體單元,只不過前者依託後者而存在,但是這並不能說前者就是後者的別名,這是矛盾的;說它對,是因為對引用變數進行操作時,改變的物件實際上都是引用的變數而不是引用變數,這就感覺引用變數只是一層偽裝,真正的還是它引用的變數,從這個角度來說,引用確實可以說是變數的別名。

7.函式傳指標與傳引用的區別。函式傳指標的原型類似於int add(int * a, int * b);這類函式的形參是指標,呼叫方式為int x = 1, y = 2; add(&x,&y);那麼傳入的實參則是變數的地址,因此在函式內部,是用地址型實參&x和&y來初始化形參a和b,相當於int *a = &x,int * b = &y的;因此形參a和b記憶體單元中存放的是x和y的地址。

而對於函式傳引用,函式原型則類似於int add(int & a,int & b),這類函式的形參是引用,呼叫方式為int x = 1, y = 2; add(x,y);傳入的實參就是x和y變數自身,在函式內部,則是用x和y來初始化a和b,相當於int &a = x,int &b = y,因此,形參a和b記憶體單元中存放的也是x和y的地址,不過它畢竟是引用,如前面所說,當你試圖對形參a或b的值進行改變的時候,改變的不是它記憶體中存放的&x和&y,其實是x或y;當你對形參a或b取地址時,取出來的並不是形參本身的地址,而是它記憶體中存放的&x和&y。

而再說一個與本文無關的函式傳值,如int add(int a,int b);這類函式的形參只是普通的int型變數,當呼叫add(x,y)時,在函式內部實際上就是用x和y來初始化a和b,相當於int a = x,int b = y,所以形參和實參實際上只是值相同而已,改變形參並不會影響實參。

相關文章