地址、指針與引用

  計算機自己是不認識程序中給的變量名,無論咱們以何種方式給變量命名,最終都會轉化爲相應的地址,編譯器會生成一些符號常量而且與對應的地址相關聯,以達到訪問變量的目的。程序員

  變量是在內存中用來存儲數據以供程序使用,變量主要有兩個部分構成:變量名、變量類型,其中變量名對應了一塊具體的內存地址,而變量類型則代表該如何翻譯內存中存儲的二級制數。咱們知道不一樣的類型翻譯爲二進制的值不一樣,好比整型是直接經過數學轉化、浮點數是採用IEEE的方法、字符則根據ASCII碼轉化,一樣變量類型決定了變量所佔的內存大小,以及如何在二進制和變量所表達的真正意義之間轉化。而指針變量也是一個變量,在內存中也佔空間,不過比較特殊的是它存儲的是其餘變量的地址。在32位的機器中,每一個進程能訪問4GB的內存地址空間,因此程序中的地址採用32位二進制數表示,也就是一個整型變量的長度,地址值通常沒有負數因此準確的說指針變量的類型應該是unsigned int 即每一個指針變量佔4個字節。還記得在定義結構體中可使用該結構體的指針做爲成員,可是不能使用該結構的實例做爲成員嗎?這是由於編譯器須要根據各個成員變量的大小分配相關的內存,用該結構體的實例做爲成員時,該結構體根本沒有定義完整,編譯器是不會知道該如何分配內存的,而任何類型的指針都只佔4個字節,編譯器天然知道如何分配內存。咱們在書寫指針變量時給定的類型是它所指向的變量的類型,這個類型決定了如何翻譯所對應內存中的值,以及該訪問多少個字節的內存。對指針的間接訪問會先先取出值,訪問到對應的內存,再根據指針所指向的變量的類型,翻譯成對應的值。通常指針只能指向對應類型的變量,好比int類型的指針只能指向int型的變量,而有一種指針變量能夠指向全部類型的變量,它就是void類型的指針變量,可是因爲這種類型的變量沒有指定它所對應的變量的類型,因此即便有了對應的地址,它也不知道該取多大內存的數據,以及如何解釋這些數據,因此這種類型的指針不支持間接訪問,下面是一個間接訪問的例子:數組

int main()
{
    int nValue = 10;
    float fValue = 10.0f;
    char cValue = 'C';
    int *pnValue = &nValue;
    float *pfValue = &fValue;
    char *pcValue = &cValue;
    printf("pnValue = %x, *pnValue = %d\n", pnValue, *pnValue);
    printf("pfValue = %x, *pfValue = %f\n", pfValue, *pfValue);
    printf("pcValue = %x, *pcValue = %c\n", pcValue, *pcValue);
    return 0;
}

下面是它對應的反彙編代碼(部分):數據結構

10:       int nValue = 10;
00401268   mov         dword ptr [ebp-4],0Ah
11:       float fValue = 10.0f;
0040126F   mov         dword ptr [ebp-8],41200000h
12:       char cValue = 'C';
00401276   mov         byte ptr [ebp-0Ch],43h
13:       int *pnValue = &nValue;
0040127A   lea         eax,[ebp-4]
0040127D   mov         dword ptr [ebp-10h],eax
14:       float *pfValue = &fValue;
00401280   lea         ecx,[ebp-8]
00401283   mov         dword ptr [ebp-14h],ecx
15:       char *pcValue = &cValue;
00401286   lea         edx,[ebp-0Ch]
00401289   mov         dword ptr [ebp-18h],edx
16:       printf("pnValue = %x, *pnValue = %d\n", pnValue, *pnValue);
0040128C   mov         eax,dword ptr [ebp-10h]
0040128F   mov         ecx,dword ptr [eax]
00401291   push        ecx
00401292   mov         edx,dword ptr [ebp-10h]
00401295   push        edx
00401296   push        offset string "pnValue = %x, *pnValue = %d\n" (00432064)
0040129B   call        printf (00401580)
004012A0   add         esp,0Ch

從上面的彙編代碼能夠看到指針變量會佔內存空間,它們的地址分別是:[ebp - 10h] 、 [ebp - 14h]、 [ebp - 18h],在給指針變量賦值時首先將變量的地址賦值給臨時寄存器,而後將寄存器的值賦值給指針變量,而經過間接訪問時也通過了一個臨時寄存器,先將指針變量的值賦值給臨時寄存器(mov     eax,dword ptr [ebp-10h])而後經過這個臨時寄存器訪問變量的地址空間,獲得變量值(     mov         ecx,dword ptr [eax]),因爲間接訪問進過了這幾步,因此在效率上是比不上直接使用變量。下面是對char型變量的間接訪問:函數

004012BF   mov         edx,dword ptr [ebp-18h]
004012C2   movsx       eax,byte ptr [edx]
004012C5   push        eax

首先也是將指針變量的值取出來,放到寄存器中,而後根據寄存器尋址找到變量對應的地址,訪問變量。其中」bye ptr「表示只操做該地址中的一個字節。spa

  對於地址咱們能夠進行加法和減法操做,地址的加法主要用於向下尋址,通常用於數組等佔用連續內存空間的數據結構,通常是地址加上一個數值,表示向後偏移必定的單位,指針一樣也有這樣的操做,可是與地址值不一樣的是指針每加一個單位,表示向後偏移一個元素,而地址值加1則就是在原來的基礎上加上一。指針偏移是根據其所指向的變量類型來決定的,好比有下面的程序:翻譯

 

int main(int argc, char* argv[])
{
    char szBuf[5] = {0x01, 0x23, 0x45, 0x67, 0x89};
    int *pInt = (int*)szBuf;
    short *pShort = (short*)szBuf;
    char *pChar = szBuf;

    pInt += 1;
    pShort += 1;
    pChar += 1;
    return 0;
}

它的彙編代碼以下:指針

 

9:        char szBuf[5] = {0x01, 0x23, 0x45, 0x67, 0x89};
00401028   mov         byte ptr [ebp-8],1
0040102C   mov         byte ptr [ebp-7],23h
00401030   mov         byte ptr [ebp-6],45h
00401034   mov         byte ptr [ebp-5],67h
00401038   mov         byte ptr [ebp-4],89h
10:       int *pInt = (int*)szBuf;
0040103C   lea         eax,[ebp-8]
0040103F   mov         dword ptr [ebp-0Ch],eax
11:       short *pShort = (short*)szBuf;
00401042   lea         ecx,[ebp-8]
00401045   mov         dword ptr [ebp-10h],ecx
12:       char *pChar = szBuf;
00401048   lea         edx,[ebp-8]
0040104B   mov         dword ptr [ebp-14h],edx
13:
14:       pInt += 1;
0040104E   mov         eax,dword ptr [ebp-0Ch]
00401051   add         eax,4
00401054   mov         dword ptr [ebp-0Ch],eax
15:       pShort += 1;
00401057   mov         ecx,dword ptr [ebp-10h]
0040105A   add         ecx,2
0040105D   mov         dword ptr [ebp-10h],ecx
16:       pChar += 1;
00401060   mov         edx,dword ptr [ebp-14h]
00401063   add         edx,1
00401066   mov         dword ptr [ebp-14h],edx

根據其彙編代碼能夠看出,對於int型的指針,每加1個會向後偏移4個字節,short會偏移2個字節,char型的會偏移1個,因此根據以上的內容,能夠得出一個公式:TYPE* P p + n = p + sizeof(TYPE) *ncode

根據上面的加法公式咱們能夠推導出兩個指針的減法公式,TYPE *p1, TYPE* p2: p2 - p1 = ((int)p2 - (int)p1) / sizeof(TYPE),兩個指針相減獲得的結果是兩個指針之間擁有元素的個數。只有同類型的指針之間才能夠相減。而指針的乘除法則沒有意義,地址之間的乘除法也沒有意義。blog

  引用是在C++中提出的,是變量的一個別名,提出引用主要是但願減小指針的使用,引用於指針在一個函數中想上述例子中那樣使用並無太大的意義,大量使用它們是在函數中,做爲參數傳遞,不只能夠節省效率,同時也能夠傳遞一段緩衝,做爲輸出參數來使用。這大大提高了程序的效率以及靈活性。可是在一些新手程序員看來指針無疑是噩夢般的存在,因此C++引入了引用,但願代替指針。在通常的C++書中都說引用是變量的一個別名是不佔內存的,可是我經過查看反彙編代碼發現引用並非向書上說的那樣,下面是一段程序及它的反彙編代碼:進程

int nValue = 10;
int &rValue = nValue;
printf("%d\n", rValue);

 

10:       int nValue = 10;
00401268   mov         dword ptr [ebp-4],0Ah
11:       int &rValue = nValue;
0040126F   lea         eax,[ebp-4]
00401272   mov         dword ptr [ebp-8],eax
12:       printf("%d\n", rValue);
00401275   mov         ecx,dword ptr [ebp-8]
00401278   mov         edx,dword ptr [ecx]
0040127A   push        edx
0040127B   push        offset string "%d\n" (0042e01c)
00401280   call        printf (00401520)

從彙編代碼中能夠看到,在定義引用併爲它賦值的過程當中,編譯器實際上是將變量的地址賦值給了一個新的變量,這個變量的地址是[ebp - 8h],在調用printf函數的時候,編譯器將地址取出並將它壓到函數棧中。下面是將引用改成指針的狀況:

10:       int nValue = 10;
00401268   mov         dword ptr [ebp-4],0Ah
11:       int *pValue = &nValue;
0040126F   lea         eax,[ebp-4]
00401272   mov         dword ptr [ebp-8],eax
12:       printf("%d\n", *pValue);
00401275   mov         ecx,dword ptr [ebp-8]
00401278   mov         edx,dword ptr [ecx]
0040127A   push        edx
0040127B   push        offset string "%d\n" (0042e01c)
00401280   call        printf (00401520)

兩種狀況的彙編代碼徹底同樣,也就是說引用其實就是指針,編譯器將其包裝了一下,使它的行爲變得和使用變量相同,並且在語法層面上作了一個限制,引用在定義的時候必須初始化,且初始化完成後就不能指向其餘變量,這個行爲與常指針相同。

相關文章
相關標籤/搜索