揭秘!值傳遞與引用傳遞,傳的到底是什么?

在網(wǎng)上看到過(guò)很多討論 Java、C++、Python 是值傳遞還是引用傳遞這類文章。
所以這一篇呢就是想從原理講明白關(guān)于函數(shù)參數(shù)傳遞的幾種形式。
參數(shù)傳遞無(wú)外乎就是傳值(pass by value),傳引用(pass by reference)或者說(shuō)是傳指針。
傳值還是傳引用可能在 Java、Python 這種語(yǔ)言中常常會(huì)困擾一些初學(xué)者,但是如果你有 C/C++背景的話,那這個(gè)理解起來(lái)就是 so easy。
今天我就從 C 語(yǔ)言出發(fā),一次性把 Java、Python 這些都給大家講明白,誰(shuí)讓我是指北呢哈哈
不過(guò)呀,要想徹底搞懂這個(gè),需要了解兩個(gè)背景知識(shí):
堆、棧 函數(shù)調(diào)用棧
一、堆、棧
要注意,這“堆”和“棧”并不是數(shù)據(jù)結(jié)構(gòu)意義上的堆(Heap,一個(gè)可看成完全二叉樹(shù)的數(shù)組對(duì)象)和 棧(Stack,先進(jìn)后出的線性結(jié)構(gòu))。
這里說(shuō)的堆棧是指內(nèi)存的兩種組織形式,堆是指動(dòng)態(tài)分配內(nèi)存的一塊區(qū)域,一般由程序員手動(dòng)分配,比如 Java 中的 new、C/C++ 中的 malloc 等,都是將創(chuàng)建的對(duì)象或者內(nèi)存塊放置在堆區(qū)。
而棧是則是由編譯器自動(dòng)分配釋放(大概就是你申明一個(gè)變量就分配一塊相應(yīng)大小的內(nèi)存),用于存放函數(shù)的參數(shù)值,局部變量等。
就拿 Java 來(lái)說(shuō)吧,基本類型(int、double、long這種)是直接將存儲(chǔ)在棧上的,而引用類型(類)則是值存儲(chǔ)在堆上,棧上只存儲(chǔ)一個(gè)對(duì)對(duì)象的引用。
舉個(gè)栗子:
int?age?=?22;
String?name?=?new?String("shuaibei");
這兩個(gè)變量存儲(chǔ)圖如下:

如果,我們分別對(duì)age、name變量賦值,會(huì)發(fā)生什么呢?
age?=?18;
name?=?new?String("xiaobei");
如下圖:

age 僅僅是將棧上的值修改為 18,而 name 由于是 String 引用類型,所以會(huì)重新創(chuàng)建一個(gè) String 對(duì)象,并且修改 name,讓其指向新的堆對(duì)象。(細(xì)心的話,你會(huì)發(fā)現(xiàn),圖中 name 執(zhí)行的地址我做了修改)
然后,之前那個(gè)對(duì)象如果沒(méi)有其它變量引用的話,就會(huì)被垃圾回收器回收掉。
這里也要注意一點(diǎn),我創(chuàng)建 String 的時(shí)候,使用的是 new,如果直接采用字符串賦值,比如:
String?name?=?"shuaibei"
那么是會(huì)放到 JVM 的常量池去,不會(huì)被回收掉,這是字符串兩種創(chuàng)建對(duì)象的區(qū)別,不過(guò)這里我們不關(guān)注。
Java 中引用這東西,和 C/C++ 的指針就是一模一樣的嘛,只不過(guò) Java 做了語(yǔ)義層包裝和一些限制,讓你覺(jué)得這是一個(gè)引用,實(shí)際上就是指針。
好,讓我繼續(xù)了解下函數(shù)調(diào)用棧。
二、函數(shù)調(diào)用棧
一個(gè)函數(shù)需要在內(nèi)存上存儲(chǔ)哪些信息呢?
參數(shù)、局部變量,理論上這兩個(gè)就夠了,但是當(dāng)多個(gè)函數(shù)相互調(diào)用的時(shí)候,就還需要機(jī)制來(lái)保證它們順利的返回和恢復(fù)主調(diào)函數(shù)的棧結(jié)構(gòu)信息。
那這部分就包括返回地址、ebp寄存器(基址指針寄存器,指向當(dāng)前堆棧底部) 以及其它需要保存的寄存器。
所以一個(gè)完整的函數(shù)調(diào)用棧大概長(zhǎng)得像下面這個(gè)樣子:

那,多個(gè)函數(shù)調(diào)用的時(shí)候呢?
簡(jiǎn)單來(lái)說(shuō)就是疊羅漢,這是兩個(gè)函數(shù)棧:

今天,我們不會(huì)去詳細(xì)了解函數(shù)調(diào)用過(guò)程ebp、ebp如何變化,返回地址又是如何起作用的。
今天的任務(wù)就是搞明白參數(shù)傳遞,所以其它的都是非主線的知識(shí),忽略即可。
順便插點(diǎn)題外話:
學(xué)習(xí)新知識(shí)有時(shí)候需要刨根問(wèn)底,有時(shí)候卻需要及時(shí)回頭,尤其是計(jì)算機(jī),你要是一直刨根問(wèn)題,我能給你整到硅的提純?nèi)ィ@就是失去了學(xué)習(xí)的意義。
最好的方式是,在一個(gè)恰到好處的地方建立一個(gè)抽象層,并且認(rèn)可這個(gè)抽象層提供的功能/接口,不去探究這一層下面是什么,怎么實(shí)現(xiàn)的。
比如,學(xué)習(xí) HTTP,我就只需要認(rèn) TCP 提供穩(wěn)定、可靠傳輸就夠了,暫時(shí)就不需要去看 TCP 如何做到的。
好了,繼續(xù)說(shuō)回函數(shù)傳參,舉個(gè)例子,下面這段代碼在main函數(shù)內(nèi)調(diào)用了func_a函數(shù)
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) 這個(gè)過(guò)程,在函數(shù)調(diào)用棧上究竟是怎么樣的呢?

就像上圖所示,編譯器會(huì)生成一段函數(shù)調(diào)用代碼。
將 main 函數(shù)內(nèi)變量 a 的值拷貝到 func_a 函數(shù)參數(shù) a 位置。
將變量 b的地址,拷貝到 func_a 函數(shù)參數(shù) b 的位置。
記住這張圖,這是函數(shù)參數(shù)傳遞的本質(zhì),沒(méi)有其它方式,just copy!
copy 意味著是副本,也就是在子函數(shù)的參數(shù)永遠(yuǎn)是主調(diào)函數(shù)內(nèi)的副本。
決定是值傳遞還是所謂的引用傳遞,在于你 copy 的到底是一個(gè)值,還是一個(gè)引用(的值)。
其實(shí)引用也是值......不要覺(jué)得引用就是那種玄乎的東西。
所以會(huì)有一種聲音說(shuō),是不存在所謂的引用傳遞的,一切傳引用的本質(zhì)還是傳值。
也就是 pass pointer by value 或者 pass reference by value,哈哈哈有點(diǎn)意思。
今天,我們不討論到底有沒(méi)有傳引用這個(gè)東西,這是一個(gè)個(gè)仁者見(jiàn)仁智者見(jiàn)智的問(wèn)題。
我的目的呢,就是把參數(shù)傳遞這個(gè)過(guò)程給大家剖析下,至于到底是傳值還是傳引用,那就看大家怎么思考了。
三、pass by value in java
舉個(gè)最簡(jiǎn)單的例子來(lái)說(shuō)明下:
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 函數(shù)實(shí)際上并沒(méi)有改變到 main 函數(shù)內(nèi)的 name 對(duì)象,看圖就明白了:

根據(jù)我們前面所講,參數(shù)傳遞實(shí)際就是復(fù)制棧上的值本身,這里name的值就一串地址,所以ChangeRef接收到的也是這串地址,但是在ChangeRef函數(shù)內(nèi)將name的指向改成了一個(gè)新的 String 對(duì)象,但是這里不會(huì)對(duì)main函數(shù)中的 name 對(duì)象產(chǎn)生任何的影響。
咦,不是說(shuō)引用類型都是引用傳遞嗎?為什么還不會(huì)對(duì)主調(diào)函數(shù)產(chǎn)生影響呢?
我們都把引用的指向改變了,還能影響個(gè)啥,如果想通過(guò)引用傳遞修改外部傳進(jìn)來(lái)的值,一般是采用成員方法。
假設(shè) String 類有一個(gè)方法叫做changeStr(String value),那么我們就可以在ChangeRef內(nèi)調(diào)用這個(gè)方法,修改name的值,
并且會(huì)同步修改到main函數(shù)里的值。
(其實(shí)這里最好的說(shuō)明方式是自己定義一個(gè)類,但是我懶了,就省掉了哈哈哈,相信聰明的你一定知道我在說(shuō)什么~)
四、Python
其實(shí)和Java 挺像的,但是 Python 有個(gè)特點(diǎn)就是所有變量本身只是一個(gè)引用,真正的類型信息都是和對(duì)象存儲(chǔ)在一起的。
所以我打算后面單獨(dú)聊聊 Python 對(duì)象這個(gè)話題,然后把參數(shù)傳遞也放在那里了,今天就到這吧~


