在網上看到過很多討論 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");
這兩個變量存儲圖如下:
如果,我們分別對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
函數
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
對象這個話題,然後把參數傳遞也放在那裏了,今天就到這吧~
本文分享自微信公衆號 - 程序員小躍(runningdimple)。
如有侵權,請聯繫 [email protected] 刪除。
本文參與“OSC源創計劃”,歡迎正在閱讀的你也加入,一起分享。