作者 | 編程指北 責編 | 張文
在網上看到過很多討論 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 = newString("shuaibei");
這兩個變量存儲圖如下:
如果,我們分別對 age、name 變量賦值,會發生什麼呢?
age = 18;name = new String("xiaobei");
如下圖:
age 僅僅是將棧上的值修改為 18,而 name 由於是 String 引用類型,所以會重新創建一個 String 對象,並且修改 name,讓其指向新的堆對象。(細心的話,你會發現,圖中 name 執行的地址我做了修改)
然後,之前那個對象如果沒有其它變量引用的話,就會被垃圾回收器回收掉。
這裡也要注意一點,我創建 String 的時候,使用的是 new,如果直接採用字符串賦值,比如:
String name = "shuaibei"
那麼是會放到 JVM 的常量池去,不會被回收掉,這是字符串兩種創建對象的區別,不過這裡我們不關注。
Java 中引用這東西,和 C/C++ 的指針就是一模一樣的嘛。只不過 Java 做了語義層包裝和一些限制,讓你覺得這是一個引用,實際上就是指針。
好,讓我繼續了解下函數調用棧。
函數調用棧
一個函數需要在內存上存儲哪些信息呢?
參數、局部變量,理論上這兩個就夠了,但是當多個函數相互調用的時候,就還需要機制來保證它們順利的返回和恢復主調函數的棧結構信息。
那這部分就包括返回地址、ebp 寄存器(基址指針寄存器,指向當前堆棧底部) 以及其它需要保存的寄存器。
所以一個完整的函數調用棧大概長得像下面這個樣子:
那,多個函數調用的時候呢?
簡單來說就是疊羅漢,這是兩個函數棧:
今天,我們不會去詳細了解函數調用過程 ebp、ebp 如何變化,返回地址又是如何起作用的。
今天的任務就是搞明白參數傳遞,所以其它的都是非主線的知識,忽略即可。
順便插點題外話:
學習新知識有時候需要刨根問底,有時候卻需要及時回頭,尤其是計算機,你要是一直刨根問題,我能給你整到矽的提純去,這就是失去了學習的意義。
最好的方式是,在一個恰到好處的地方建立一個抽象層,並且認可這個抽象層提供的功能/接口,不去探究這一層下面是什麼,怎麼實現的。
比如,學習 HTTP,我就只需要認 TCP 提供穩定、可靠傳輸就夠了,暫時就不需要去看 TCP 如何做到的。
好了,繼續說回函數傳參,舉個例子,下面這段代碼在 main 函數內調用了 func_a 函數
intfunc_a(int a, int *b){ a = 5; *b = 5;};intmain(void){int a = 10;int b = 10; func_a(a, &b);printf("a=%d, b=%d\n", a, b);return0;}// 輸出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
舉個最簡單的例子來說明下:
publicclassHelloWorld {publicstaticvoidChangeRef(String name) { name = new String("xiaobei"); }publicstaticvoidmain(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 有個特點就是所有變量本身只是一個引用,真正的類型信息都是和對象存儲在一起的。這裡就不再過多展開。
以上,希望能幫到大家。