揭祕!值傳遞與引用傳遞,傳的到底是什麼?

在網上看到過很多討論 Java、C++、Python 是值傳遞還是引用傳遞這類文章。

所以這一篇呢就是想從原理講明白關於函數參數傳遞的幾種形式。

參數傳遞無外乎就是傳值(pass by value),傳引用(pass by reference)或者說是傳指針。

傳值還是傳引用可能在 Java、Python 這種語言中常常會困擾一些初學者,但是如果你有 C/C++背景的話,那這個理解起來就是 so easy。

今天我就從 C 語言出發,一次性把 Java、Python 這些都給大家講明白,誰讓我是指北呢哈哈

不過呀,要想徹底搞懂這個,需要了解兩個背景知識:

  • 堆、棧
  • 函數調用棧

一、堆、棧

要注意,這“堆”和“棧”並不是數據結構意義上的堆(Heap,一個可看成完全二叉樹的數組對象)和 棧(Stack,先進後出的線性結構)。

這裏說的堆棧是指內存的兩種組織形式,堆是指動態分配內存的一塊區域,一般由程序員手動分配,比如 Java 中的 new、C/C++ 中的 malloc 等,都是將創建的對象或者內存塊放置在堆區。

而棧是則是由編譯器自動分配釋放(大概就是你申明一個變量就分配一塊相應大小的內存),用於存放函數的參數值,局部變量等。

就拿 Java 來說吧,基本類型(int、double、long這種)是直接將存儲在棧上的,而引用類型(類)則是值存儲在堆上,棧上只存儲一個對對象的引用。

舉個栗子:

int age = 22;
String name = new String("shuaibei");

這兩個變量存儲圖如下:

如果,我們分別對agename變量賦值,會發生什麼呢?

age = 18;
name = new String("xiaobei");

如下圖:

age 僅僅是將棧上的值修改爲 18,而 name 由於是 String 引用類型,所以會重新創建一個 String 對象,並且修改 name,讓其指向新的堆對象。(細心的話,你會發現,圖中 name 執行的地址我做了修改)

然後,之前那個對象如果沒有其它變量引用的話,就會被垃圾回收器回收掉。

這裏也要注意一點,我創建 String 的時候,使用的是 new,如果直接採用字符串賦值,比如:

String name = "shuaibei"

那麼是會放到 JVM 的常量池去,不會被回收掉,這是字符串兩種創建對象的區別,不過這裏我們不關注。

Java 中引用這東西,和 C/C++ 的指針就是一模一樣的嘛,只不過 Java 做了語義層包裝和一些限制,讓你覺得這是一個引用,實際上就是指針。

好,讓我繼續瞭解下函數調用棧。

二、函數調用棧

一個函數需要在內存上存儲哪些信息呢?

參數、局部變量,理論上這兩個就夠了,但是當多個函數相互調用的時候,就還需要機制來保證它們順利的返回和恢復主調函數的棧結構信息。

那這部分就包括返回地址、ebp寄存器(基址指針寄存器,指向當前堆棧底部) 以及其它需要保存的寄存器。

所以一個完整的函數調用棧大概長得像下面這個樣子:

那,多個函數調用的時候呢?

簡單來說就是疊羅漢,這是兩個函數棧:

今天,我們不會去詳細瞭解函數調用過程ebpebp如何變化,返回地址又是如何起作用的。

今天的任務就是搞明白參數傳遞,所以其它的都是非主線的知識,忽略即可。

順便插點題外話:

學習新知識有時候需要刨根問底,有時候卻需要及時回頭,尤其是計算機,你要是一直刨根問題,我能給你整到硅的提純去,這就是失去了學習的意義。

最好的方式是,在一個恰到好處的地方建立一個抽象層,並且認可這個抽象層提供的功能/接口,不去探究這一層下面是什麼,怎麼實現的。

比如,學習 HTTP,我就只需要認 TCP 提供穩定、可靠傳輸就夠了,暫時就不需要去看 TCP 如何做到的。

好了,繼續說回函數傳參,舉個例子,下面這段代碼在main函數內調用了func_a函數

int func_a(int a, int *b) {
 a = 5;
 *b = 5;
};

int main(void) {
 int a = 10;
  int b = 10;
  func_a(a, &b);
  printf("a=%d, b=%d\n", a, b);
  return 0;
}

// 輸出
a=10, b=5

那麼func_a(a, &b) 這個過程,在函數調用棧上究竟是怎麼樣的呢?

就像上圖所示,編譯器會生成一段函數調用代碼。

main 函數內變量 a 的值拷貝到 func_a 函數參數 a 位置。

將變量 b的地址,拷貝到 func_a 函數參數 b 的位置。

記住這張圖,這是函數參數傳遞的本質,沒有其它方式,just copy!

copy 意味着是副本,也就是在子函數的參數永遠是主調函數內的副本。

決定是值傳遞還是所謂的引用傳遞,在於你 copy 的到底是一個值,還是一個引用(的值)。

其實引用也是值......不要覺得引用就是那種玄乎的東西。

所以會有一種聲音說,是不存在所謂的引用傳遞的,一切傳引用的本質還是傳值。

也就是 pass pointer by value 或者 pass reference by value,哈哈哈有點意思。

今天,我們不討論到底有沒有傳引用這個東西,這是一個個仁者見仁智者見智的問題。

我的目的呢,就是把參數傳遞這個過程給大家剖析下,至於到底是傳值還是傳引用,那就看大家怎麼思考了。

三、pass by value in java

舉個最簡單的例子來說明下:

public class HelloWorld {
  
    public static void ChangeRef(String name) {
        name = new String("xiaobei");
    }

    public static void main(String[] args) {
        String name = new String("shuaibei");
       ChangeRef(name);
        System.out.println(name.equals("shuaibei"));

    }
}

上面,ChangeRef 函數實際上並沒有改變到 main 函數內的 name 對象,看圖就明白了:

根據我們前面所講,參數傳遞實際就是複製棧上的值本身,這裏name的值就一串地址,所以ChangeRef接收到的也是這串地址,但是在ChangeRef函數內將name的指向改成了一個新的 String 對象,但是這裏不會對main函數中的 name 對象產生任何的影響。

咦,不是說引用類型都是引用傳遞嗎?爲什麼還不會對主調函數產生影響呢?

我們都把引用的指向改變了,還能影響個啥,如果想通過引用傳遞修改外部傳進來的值,一般是採用成員方法。

假設 String 類有一個方法叫做changeStr(String value),那麼我們就可以在ChangeRef內調用這個方法,修改name的值,

並且會同步修改到main函數裏的值。

(其實這裏最好的說明方式是自己定義一個類,但是我懶了,就省掉了哈哈哈,相信聰明的你一定知道我在說什麼~)

四、Python

其實和Java 挺像的,但是 Python 有個特點就是所有變量本身只是一個引用,真正的類型信息都是和對象存儲在一起的。

所以我打算後面單獨聊聊 Python 對象這個話題,然後把參數傳遞也放在那裏了,今天就到這吧~





0、離開一線城市的程序員們後悔了嗎?這裏有一些真實的打工人體驗

1、程序員小躍:三十而立,和大家一起「乘風破浪」

本文分享自微信公衆號 - 程序員小躍(runningdimple)。
如有侵權,請聯繫 [email protected] 刪除。
本文參與“OSC源創計劃”,歡迎正在閱讀的你也加入,一起分享。

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章