新聞中心
本文轉(zhuǎn)載自微信公眾號「crossoverJie 」,作者crossoverJie 。轉(zhuǎn)載本文請聯(lián)系crossoverJie公眾號。

創(chuàng)新互聯(lián)專注于雨花臺企業(yè)網(wǎng)站建設(shè),響應式網(wǎng)站開發(fā),購物商城網(wǎng)站建設(shè)。雨花臺網(wǎng)站建設(shè)公司,為雨花臺等地區(qū)提供建站服務(wù)。全流程按需求定制開發(fā),專業(yè)設(shè)計,全程項目跟蹤,創(chuàng)新互聯(lián)專業(yè)和態(tài)度為您提供的服務(wù)
前言
最近一年多的時間陸續(xù)接觸了一些對我來說陌生的語言,主要就是 Python 和 Go,期間為了快速實現(xiàn)需求只是依葫蘆畫瓢的擼代碼;并沒有深究一些細節(jié)與原理。
就拿參數(shù)傳遞一事來說各個語言的實現(xiàn)細節(jié)各不相同,但又有類似之處;在許多新手入門時容易搞不清楚,導致犯一些低級錯誤。
Java
基本類型傳遞
先拿我最熟悉的 Java 來說,我相信應該沒人會寫這樣的代碼:
- @Test
- public void testBasic() {
- int a = 10;
- modifyBasic(a);
- System.out.println(String.format("最終結(jié)果 main a==%s", a));
- }
- private void modifyBasic(int aa) {
- System.out.println(String.format("修改之前 aa==%s", aa));
- aa = 20;
- System.out.println(String.format("修改之后 aa==%s", aa));
- }
輸出結(jié)果:
- 修改之前 aa==10
- 修改之后 aa==20
- 最終結(jié)果 main a==10
不過從這段代碼的目的來看應該是想要修改 a 的值,從直覺上來說如果修改成功也是能理解的。
至于結(jié)果與預期不符合的根本原因是理解錯了參數(shù)的值傳遞與引用傳遞。
在這之前還是先明確下值傳遞與引用傳遞的區(qū)別:
這里咱們先拋出結(jié)論,Java 采用的是值傳遞;這樣也能解釋為什么上文的例子沒有成功修改原始數(shù)據(jù)。
參考下圖更好理解:
當發(fā)生函數(shù)調(diào)用的時候 a 將自己傳入到 modifyBasic 方法中,同時將自己的值復制了一份并賦值給了一個新變量 aa 從圖中可以看出這是 a 和 aa 兩個變量沒有一毛錢關(guān)系,所以對 aa 的修改并不會影響到 a。
有點類似于我把蘋果給了老婆,她把蘋果削好了;但我手里這顆并沒有變化,因為她只是從餐盤里拿了一顆一模一樣的蘋果削好了。
如果我想要她那顆,只能讓她把削好的蘋果給我;也就類似于使用方法的返回值。
- a = modifyBasic(a);
引用類型傳遞
下面來看看引用類型的傳遞:
- private class Car{
- private String name;
- public Car(String name) {
- this.name = name;
- }
- @Override
- public String toString() {
- return "Car{" +
- "name='" + name + '\'' +
- '}';
- }
- }
- @Test
- public void test01(){
- Car car1 = new Car("benz");
- modifyCar1(car1);
- System.out.println(String.format("最終結(jié)果 main car1==%s", car1));
- }
- private void modifyCar1(Car car){
- System.out.println(String.format("修改之前 car==%s", car));
- car.name = "bwm";
- System.out.println(String.format("修改之后 car==%s", car));
- }
在這個例子里先創(chuàng)建了一個 benz 的 car1,通過一個方法修改為 bmw 那最開始的 car1 會受到影響嘛?
- 修改之前 car==Car{name='benz'}
- 修改之后 car==Car{name='bwm'}
- 最終結(jié)果 main car1==Car{name='bwm'}
結(jié)果可能會與部分人預期相反,這樣的修改卻是可以影響到原有數(shù)據(jù)的?這豈不是和值傳遞不符,看樣子這是引用傳遞吧?
別急,通過下圖分析后大家就能明白:
在 test01 方法中我們創(chuàng)建了一個 car1 的對象,該對象存放于堆內(nèi)存中,假設(shè)內(nèi)存地址為 0x1102 ,于是 car1 這個變量便應用了這塊內(nèi)存地址。
當我們調(diào)用 modifyCar1 這個方法的時候會在該方法棧中創(chuàng)建一個變量 car ,接下來重點到了:
這個 car 變量是由原本的入?yún)?car1 復制而來,所以它所對應的堆內(nèi)存依然是 0x1102;
所以當我們通過 car 這個變量修改了數(shù)據(jù)后,本質(zhì)上修改的是同一塊堆內(nèi)存中的數(shù)據(jù)。從而原本引用了這塊內(nèi)存地址的 car1 也能查看到對應的變化。
這里理解起來可能會比較繞,但我們記住一點就行:
傳遞引用類型的數(shù)據(jù)時,傳遞的并不是引用本身,依然是值;只是這個值 是內(nèi)存地址罷了。
因為把相同的內(nèi)存地址傳過去了,所以對數(shù)據(jù)的操作依然會影響到外部。
所以同理,類似于這樣的代碼也會影響到外部原始數(shù)據(jù):
- @Test
- public void testList(){
- List
list = new ArrayList<>(); - list.add(1);
- addList(list);
- System.out.println(list);
- }
- private void addList(List
list) { - list.add(2);
- }
- [1, 2]
那如果是這樣的代碼:
- @Test
- public void test02(){
- Car car1 = new Car("benz");
- modifyCar(car1);
- System.out.println(String.format("最終結(jié)果 main car1==%s", car1));
- }
- private void modifyCar(Car car2) {
- System.out.println(String.format("修改之前 car2==%s", car2));
- car2 = new Car("bmw");
- System.out.println(String.format("修改之后 car2==%s", car2));
- }
假設(shè) Java 是引用傳遞那最終的結(jié)果應該是打印 bmw 才對。
- 修改之前 car2==Car{name='benz'}
- 修改之后 car2==Car{name='bmw'}
- 最終結(jié)果 main car1==Car{name='benz'}
從結(jié)果又能佐證這里依然是值傳遞。
如果是引用傳遞,原本的 0x1102 應該是被直接替換為新創(chuàng)建的 0x1103 才對;而實際情況如上圖所示,car2 直接重新引用了一個對象,兩個對象之間互不干擾。
Go
相對于 Java 來說 Go 的用法又有所不同,不過我們也可以先得出結(jié)論:
Go語言的參數(shù)也是值傳遞。
在 Go 語言中數(shù)據(jù)類型主要有以下兩種:
值類型與引用類型;
值類型
先以值類型舉例:
- func main() {
- a :=10
- modifyValue(a)
- fmt.Printf("最終 a=%v", a)
- }
- func modifyValue(a int) {
- a = 20
- }
- 輸出:最終 a=10
函數(shù)調(diào)用過程與之前的 Java 類似,本質(zhì)上傳遞到函數(shù)中的值也是 a 的拷貝,所以對其的修改不會影響到原始數(shù)據(jù)。
當我們把代碼稍加修改:
- func main() {
- a :=10
- fmt.Printf("傳遞之前a的內(nèi)存地址%p \n", &a)
- modifyValue(&a)
- fmt.Printf("最終 a=%v", a)
- }
- func modifyValue(a *int) {
- fmt.Printf("傳遞之后a的內(nèi)存地址%p \n", &a)
- *a = 20
- }
- 傳遞之前a的內(nèi)存地址0xc0000b4040
- 傳遞之后a的內(nèi)存地址0xc0000ae020
- 最終 a=20
從結(jié)果來看最終 a 的值是被方法修改了,這點便是 Go 與 Java 很大的不同點:
在 Go 中存在著指針的概念,我們可以將變量通過指針的方式傳遞到不同的方法中,在方法里便可通過這個指針訪問甚至修改原始數(shù)據(jù)。
那這么一看不就是引用傳遞嘛?
其實不然,我們仔細看看剛才的輸出會發(fā)現(xiàn)參數(shù)傳遞前后的內(nèi)存地址并不相同。
- 傳遞之前a的內(nèi)存地址0xc0000b4040
- 傳遞之后a的內(nèi)存地址0xc0000ae020
這也恰好論證了值傳遞,因為這里實際傳遞的是指針的拷貝。
也就是說 modifyValue 方法中的參數(shù)與入?yún)⒌?a都是同一塊內(nèi)存的指針,但指針本身也是需要內(nèi)存來存放的,所以在方法調(diào)用過程中新建了一個指針 a ,從而導致他們的內(nèi)存地址不同。
雖然內(nèi)存地址不同,但指向的數(shù)據(jù)都是同一塊,所以方法內(nèi)修改后原始數(shù)據(jù)也受到了影響。
引用類型
對于 map slice channel 這類引用類型又略有不同:
- func main() {
- var personList = []string{"張三","李四"}
- modifySlice(personList)
- fmt.Printf("slice=%v \n", personList)
- }
- func modifySlice(personList []string) {
- personList[1] = "王五"
- }
- slice=[張三 王五]
最終我們會發(fā)現(xiàn)原始數(shù)據(jù)也被修改了,但我們并沒有傳遞指針;同樣的特性也適用于 map 。
但其實我們查看 slice 的源碼會發(fā)現(xiàn)存放數(shù)據(jù)的 array 就是指針類型:
- type slice struct {
- array unsafe.Pointer
- len int
- cap int
- }
所以我們可以直接對數(shù)據(jù)進行修改,相當于間接的帶了指針。
使用建議
那我們在什么時候使用指針呢?有以下幾點建議:
- 如果參數(shù)是基本的值類型,比如 int,float 建議直接傳值。
- 如果需要修改基本的值類型,那只能是指針;但考慮到代碼可讀性還是建議將修改后的值返回用于重新賦值。
- 數(shù)據(jù)量較大時建議使用指針,減少不必要的值拷貝。(具體多大可以自行判斷)
Python
在 Python 中變量是否可變是影響參數(shù)傳遞的重要因素:
如上圖所示,bool int float 這些不可變類型在參數(shù)傳遞過程中是不能修改原始數(shù)據(jù)的。
- if __name__ == '__main__':
- x = 1
- modify(x)
- print('最終 x={}'.format(x))
- def modify(val):
- val = 2
- 最終 x=1
原理與 Java Go中類似,是基于值傳遞的,這里就不再復述。
這里重點看看可變數(shù)據(jù)類型在參數(shù)傳遞中的過程:
- if __name__ == '__main__':
- x = [1]
- modify(x)
- print('最終 x={}'.format(x))
- def modify(val):
- val.append(2)
- 最終 x=[1, 2]
最終數(shù)據(jù)受到了影響,那么就表明這是引用傳遞嘛?再看個例子試試:
- if __name__ == '__main__':
- x = [1]
- modify(x)
- print('最終 x={}'.format(x))
- def modify(val):
- val = [1, 2, 3]
- 最終 x=[1]
顯而易見這并不是引用傳遞,如果是引用傳遞最終 x 應當?shù)扔?[1, 2 ,3] 。
從結(jié)果來看這個傳遞過程非常類似 Go 中的指針傳遞,val 拿到的也是 x 這個參數(shù)內(nèi)存地址的拷貝;他們都指向了同一塊內(nèi)存地址。
所以對這塊數(shù)據(jù)的修改本質(zhì)上改的是同一份數(shù)據(jù),但一旦重新賦值就會創(chuàng)建一塊新的內(nèi)存從而不會影響到原始數(shù)據(jù)。
與 Java 中的上圖類似。
所以總結(jié)下:
- 對于不可變數(shù)據(jù):在參數(shù)傳遞時傳遞的是值,對參數(shù)的修改不會影響到原有數(shù)據(jù)。
- 對于可變數(shù)據(jù):傳遞的是內(nèi)存地址的拷貝,對參數(shù)的操作會影響到原始數(shù)據(jù)。
這么說來這三種都是值傳遞了,那有沒有引用傳遞的語言呢?
當然,C++是支持引用傳遞的:
- #include
- using namespace std;
- class Box
- {
- public:
- double len;
- };
- void modify(Box& b);
- int main ()
- {
- Box b1;
- b1.len=100;
- cout << "調(diào)用前,b1 的值:" << b1.len << endl;
- modify(b1);
- cout << "調(diào)用后,b1 的值:" << b1.len << endl;
- return 0;
- }
- void modify(Box& b)
- {
- b.len=10.0;
- Box b2;
- b2.len = 999;
- b = b2;
- return;
- }
- 調(diào)用前,b1 的值:100
- 調(diào)用后,b1 的值:999
可以看到把新對象 b2 賦值給入?yún)?b 后是會影響到原有數(shù)據(jù)的。
總結(jié)
其實這幾種語言看下來會發(fā)現(xiàn)他們中也有許多相似之處,所以通常我們在掌握一門語言后也能快速學習其他語言。
但往往是這些基礎(chǔ)中的基礎(chǔ)最讓人忽略,希望大家在日常編碼時能夠考慮到這些基礎(chǔ)知識多想想一定會寫出更漂亮的代碼(bug)。
當前名稱:一文搞懂參數(shù)傳遞原理
網(wǎng)址分享:http://www.5511xx.com/article/dpphioj.html


咨詢
建站咨詢
