2020-12-6(从反汇编理解指针和引用的区别)
這是我10個月前看到的一篇博客吧,感覺分析指針和引用的文章這是我目前見過講解得最清晰的一篇:
本文主要基于反匯編代碼,從初始化、賦值以及取地址三個角度來理解指針和引用的區別。
初始化
寫出以下代碼并查看反匯編代碼:
在初始化階段,指針和引用的行為都是一樣的:先將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叫做“引用變量”,來表明它也是一個有地址的“變量”(這種說法并不準確,只是暫時找不到其它說法)。
賦值
由于自加自減也是一個賦值的過程,為了便于敘述,這里就用自加來進行分析。寫出以下代碼并查看反匯編代碼:
從反匯編代碼可以看到,指針的自加和引用的自加是不同的,引用的步驟更多。
在指針方面,一共有三步:①把指針變量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可以不用管它,只需要觀察取地址的過程。查看相應反匯編代碼:
由于不需要管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,所以形參和實參實際上只是值相同而已,改變形參并不會影響實參。
總結
以上是生活随笔為你收集整理的2020-12-6(从反汇编理解指针和引用的区别)的全部內容,希望文章能夠幫你解決所遇到的問題。
- 上一篇: 2020-12-5(操作系统---设备管
- 下一篇: JUSTCTF校赛安卓wp