我對變數產生了這些想法

大愚Talk發表於2018-06-02

最近在學習Golang的過程中,發現一個有意思的事情,有的文章說函式呼叫傳參時 slice 是引用傳遞,有的說是值傳遞。為什麼同一個東西大家會不同認識?為了搞清楚其本質,我進行了以下內容的研究:

  1. 變數的變數名、變數值、變數地址在記憶體中是怎麼樣的?
  2. 指標的定義是什麼?引用的定義是什麼?二者有什麼關係?
  3. 函式傳參中值傳遞、指標傳遞與引用傳遞到底有什麼不一樣?
  4. Go中 slice 在傳入函式時到底是不是引用傳遞?如果不是,在函式內為什麼能修改其值?

為了避免文章寫的過長,看了想瞌睡,分成兩篇文章來解釋這個問題,本文先解決問題1跟2,下一篇說明餘下的問題。

變數名程式設計師給地址取的外號

上學的時候,老師講變數是存在記憶體中的,記憶體就像一排排抽屜組成的,每個抽屜上面有個編號,我們定義一個變數,就是把想放的東西放到這個對應編號的抽屜裡。比如: int a = 10,用圖來表示下:

ref1

這裡:變數的名字叫 a ,變數的值是:10,變數的地址是:0x 00000001。 那麼問題來了,變數的值我們知道是放在了抽屜裡(記憶體中),每個抽屜有編號(地址),但是變數的名字 a 存放在哪裡呢?或者說它會存在於記憶體中嗎?

大家想一個問題,如果變數的名字要存放在記憶體中,那麼肯定分配一個空間給他,儲存它的空間有個地址,這個地址是不是又得有個地方存起來程式才能找到?如果真是這樣設計,那麼程式碼根本沒發寫、無法執行了。

其實變數名僅僅是寫給程式設計師看的,讓我們寫程式碼的時候知道這個變數有什麼用,能夠通過名字呼叫變數的值。因為如果直接給你一個地址 0x 23004123,你知道這是要幹嘛嗎?程式碼經過編譯後,最終都會轉換成機器碼,我們定義的變數名就都不存在了,存在的只有地址跟值。

指標其實很普通

有了上面的理解,再來一個特殊的變數:指標變數。什麼叫指標變數呢?其實就是這個變數裡邊存放的是一個變數的地址,通過這個地址,機器可以找到對應變數的值,例如:int * pa = &a,就表示變數 pa 抽屜裡放的是 a 的地址,它的型別是:int*,繼續看圖:

ref2

這裡需要重要說明的是:指標pa與a的關係是:a抽屜裡邊放的是變數值10,pa放的是變數的地址:0x00000001,這裡一定要記住,下面說引用的時候才更容易理解。

引用就是變數的另一名字

繼續談引用,引用與指標我們經常傻傻分不清,因為它們的行為確實非常詭異,看起來效果非常相似,看程式碼:

由於引用的概念是在 c++ 中引入的,因此下面的程式碼使用c++,僅僅是一些列印而已,放心看下去


int main() {
    int a = 10;// 變數
    int * pa = &a; // 指標
    int & b = a; // 引用

    printf("a: %d\n", a);// a: 10
    printf("*pa: %d\n", *pa);// *pa: 10
    printf("b: %d\n", b);// b: 10

    *pa = 20;
    printf("a: %d\n", a);// a: 20
    printf("*pa: %d\n", *pa);// *pa: 20
    printf("b: %d\n", b);// b: 20

    b = 30;
	  printf("a: %d\n", a);// a: 30
    printf("*pa: %d\n", *pa);// *pa: 30
    printf("b: %d\n", b);// b: 30

	  a = 40;
	  printf("a: %d\n", a);// a: 40
    printf("*pa: %d\n", *pa);// *pa: 40
    printf("b: %d\n", b);// b: 40
    return 0;
}
複製程式碼

通過上面的程式碼我們發現,指標與引用都能達到一個效果:都有能力修改a的值,指標前面講過了,因為它儲存了a的地址,通過解引用操作後,實際上就是開啟了a的抽屜,因此可以進行修改。那麼引用又是怎麼辦到的?這裡注意一個細節:*pa = 20; c = 30;a = 40。我們看到操作c的時候與操作a是一樣的方式:直接使用變數名,但是pa要想改變a的值,必須進行 *pa 操作(解引用),如果直接 pa=20,這僅僅是改變的pa的值,讓他指向了另外一個地址。

為什麼引用與變數是一樣的操作方式?先來看一下引用的定義:

引用就是某一變數的一個別名,對引用的操作與對變數直接操作完全一樣。

那麼別名是什麼意思呢?繼續看圖,一看就懂

ref3
看到了吧?a就是b,b就是a。系統並不會為引用額外分配空間進行儲存,甚至可以簡單理解為:這個別名僅僅是為了給程式設計師看的,到機器碼層面的時候,他們都會變成地址:0x 00000001。

有碼為證

通過上面的分析不知道你理解了幾分?或者你是不是對指標與引用還是半信半疑?沒關係,寫點程式碼證明一下即可,我們要證明的是:

  • 引用是變數的別名,那麼它的地址應該與變數一致;
  • 指標儲存的是變數的地址,那麼它的值是變數的地址,它自身的地址與變數不同。

為了證明,程式設計如下:定義一個變數,分別賦值給指標、引用,然後檢查他們對應的值與地址。

int main() {
    int a = 10;
    printf("%d\n", a);
    printf("%p\n", &a);

    printf("~~~~~~~~~~~~~~\n");
    int * b = &a;
    printf("%p\n", b);
    printf("%p\n", &b);

    printf("~~~~~~~~~~~~~~\n");
    int & c = a;
    printf("%d\n", c);
    printf("%p\n", &c);
    return 0;
}
複製程式碼

獲得輸出:

10 // 變數a的值
0x7ffee3c7a768 // 變數a的地址
~~~~~~~~~~~~~~
0x7ffee3c7a768 // 指標的值,是變數a的地址
0x7ffee3c7a760 // 指標變數自己的地址
~~~~~~~~~~~~~~
10 // 變數a的值
0x7ffee3c7a768 // 引用變數c的地址,與變數a的地址完全一樣
複製程式碼

在上面如果指標想要列印變數a的值,需要解引用操作:printf(“%d\n”, *b);

小結

  • 變數由三分部分構成:變數名、變數值、變數地址;
  • 變數名實際上只是給程式設計師看的,編譯後的程式碼中並不存在變數名;
  • 指標變數就是一個變數儲存了另外一個變數的地址,系統也會為他分配記憶體空間來儲存這個地址;
  • 引用實際是變數的別名,他跟變數有相同的地址。

下次預告:

  1. 函式傳參中值傳遞、指標傳遞與引用傳遞到底有什麼不一樣?
  2. 為什麼說 slice 是引用型別?
  3. Go中 slice 在傳入函式時到底是不是引用傳遞?如果不是,在函式內為什麼能修改其值?

相關文章