1. 程式人生 > >按值傳遞 vs. 按指標傳遞

按值傳遞 vs. 按指標傳遞

按值傳遞還是指標傳遞?

變數賦值有兩種方式:按值傳遞、按"指標"傳遞(指標也常稱為"引用")。不同的程式語言賦值的方式不一樣,例如Python是按"指標"傳遞的,Go是按值傳遞的。

注意,"指標"加了引號,因為它不是真正的按指標拷貝,見下文分析。

引數傳值其實也是變數賦值的過程,只不過引數是函式的本地變數而已。

按值傳遞的意思是每次賦值都拷貝記憶體中完整的資料結構物件,這時在記憶體中會儲存兩份內容完全相同,但地址不同的資料物件。

按"指標"傳遞的意思是每次賦值都只拷貝記憶體中資料結構物件的地址,這個地址佔用一個機器字長(一個機器字長,在32位cpu上為32bit共4位元組,64位則64bit共8位元組),當然有些資料結構除了指標還包括其它屬性,這時可能會佔用數個機器字長。總之,按"指標"傳遞時,由於只拷貝一份能表示資料物件的屬性(比如地址),拷貝的內容非常少,速度非常快。但必須注意,拷貝"指標"後,記憶體中只有一份資料物件,但將有兩份完全相同的指向記憶體中資料物件的"指標",無論是通過哪個"指標"去修改資料物件,都會影響另一個。

對於那些不支援操作指標的語言,通常會將按"指標"傳遞稱為"淺拷貝(shallow copy)",然後額外提供一個函式或工具實現按指傳遞,這稱為"深拷貝(deep copy)"。

例如:

a=10
b=a

首先會在記憶體中劃分一個格子用來建立資料物件10,然後將這個資料物件的地址儲存到變數a中。

如果是按值拷貝的語言,則會在記憶體中拷貝一份資料物件10的副本,再將這個副本資料物件的地址儲存到b中。

顯然,a和b儲存的地址是不一樣的,記憶體中也有兩份內容完全相同的資料物件10。所以,修改a的值時不會影響b的值,修改b的值時不會影響a。

如果是按"指標"拷貝的語言,則會直接拷貝a中的地址並儲存到b中。

因為a和b的地址都一樣,所以,修改a的值會影響b,修改b的值會影響a。

也許你已經發現了,按"指標"傳遞時,雖然a、b儲存的地址相同,但如果a=11,a將指向新的資料物件,而b仍然指向10,即b=10,修改a並沒有影響b。這是因為數值是不可變的,無法在原始的記憶體地址處修改,也就是無法將10替換成11,所以只要想修改這種不可變的物件就一定會建立新資料物件。對此,有兩方面需要說明。

一方面,有些資料物件是可以在原始記憶體地址處直接進行替換修改的(例如python中的列表)。假設,某程式語言對數值也是可原處修改的,那麼a=11將會在記憶體中將10替換成11,而不會新建立另一個數據物件11。

另一方面,上面的"按指標傳遞"並非是真正的按指標傳遞,而是按引用傳遞,或者說是按地址傳遞。這就是前文"按指標傳遞"中的"指標"都加上了引號的原因。

真正的指標是額外儲存的,是佔用空間的,和變數不同(變數儲存了地址,在棧空間中),它是儲存在堆記憶體中的。對於支援指標操作的語言(如C、C++、Go等),需要使用語法獨立生成資料物件的指標,這類語言一般都能直接在原處修改資料物件。例如:

a=10
b=&a

其中b=&a表示生成a所指向(因為a儲存了地址)資料物件的一個額外的指標,這個指標中儲存了資料物件的地址,然後將這個指標賦值給b,這時b儲存的是指標的地址,而不是資料物件的地址。

這時,修改a,或者修改b都會影響另一方,因為支援指標操作的語言一般都支援原處修改:

a=11
print(*b)  /* 輸出11 */

其中*b表示解除指標的引用,也就是取得資料物件的內容。

再回到按"指標"傳遞的拷貝方式,雖然它不是真正的拷貝指標,而是拷貝地址,但對於那些支援原處修改的資料物件,它們達到的效果和真實的指標傳遞是一樣的。例如,陣列、python的列表。

# 以下為python程式碼:
L1=[1,2,3,4]
L2=L1
L2[0]=11
print(L1)   # 輸出:[11,2,3,4]