Call By Value, Call By Reference? 談談所謂的參數引用

參數引用

大家應該都聽過 Call By Value,Call By Reference 或是 Call By Sharing

這些都是很常見的函式參數引用原則

如果沒聽過相灣概念的話,下面會簡單的做一下介紹

Call By Value

Call by value 是一種參數引用的方式,大部分的人接觸到函式這一塊時,不知不覺中都會使用到這種形式的參數引用

舉個 C 的例子:

void foo(int arg1, int arg2) { }

在這個例子中,你可以看到一個函式foo,裡面有2個 int 參數args1args2

這裡的arg1arg2都是Call by value的形式,意思是它就單純是個數值,跟原本的變數毫無關聯

這邊舉一個新手常犯的錯誤:

void add(int a, int b) {
a = a+b;
}

int main() {
int a = 5;
int b = 6;
add(a, b); // 應該要是 5+6=11
print("a = %d", a); // 這邊印出的還是 a = 5
return 0;
}

這邊的輸出會與原本的期望a = 11不符,反而還是a = 5

這邊即說明了 Call by value 並不會改變它所引用的變數,因為它引用的僅僅只是該變數當時的數值,並不是該變數本身

Call by value 在每個程式語言中都一定會被用到
通常你所使用的函式參數沒有加什麼特別的符號或關鍵字,都會是Call by value

Call By Reference

Call by reference 跟 Call by value 最大的差別是,他是引用變數而非數值

就拿剛剛的add函式來舉個例子:

void add(int *a, int *b) {
*a = *a + *b;
}

int main() {
int a = 5;
int b = 6;
add(&a, &b); // 應該要是 5+6=11
print("a = %d", a); // 這邊印出的是 a = 11
return 0;
}

這邊跟剛剛的不同之處在於,add函式的參數型態從原本的int變成int*,也就是指向型別為 int 的指標

不清楚什麼是指標的人,這邊就簡單說明一下
指標其實就是該資料的記憶體位置
這個記憶體位置基本上就是一個整數 (int),或是長整數 (long)

main函式中透過傳遞ab的位址呼叫add,來達到a=a+b的效果

如果你仔細觀察一下上面那句話,你就會發現我剛剛說的是傳遞位址

看出端倪了嗎?其實它實際上還是 Call by value,但是它傳遞的是記憶體位址,最後可以透過*取值運算子取得數值(僅適用於 C/C++ 等語言)

在其他高階程式語言中,比較看不出來他是傳遞記憶體位址,因為要避免使用者亂動記憶體的數值,因此這時候的 Call by reference 會是以其他符號或是關鍵字來表示

以 C# 來說, C# 的 Call by reference 可以是ref或是out,兩者的差別在於一個要有初始值才能引用,另一個則可以保證一定有輸出值

以 php 來說,雖然一樣是在參數名稱前加上&運算子,但是使用時不需要使用*取值做設定跟引用(C++亦同)

值得一提的是,並不是每個程式語言都有提供 Call by reference 的功能,像是 python 就沒有可以使用 Call by reference的關鍵字

Call By Sharing

Call by sharing 其實並不是一個正式的名詞,我第一次看到這個名詞是在尋找 Java 的相關資料時看到的

它是在解釋發生在像是 java 這種物件導向程式語言上的某種詭異現象

java 並沒有 Call by reference 的關鍵字,但是在某些時候,卻又有跟 Call by reference 一樣功能的情況

舉例來說:

static class ClassA {
public int value = 0;

public static void foo(ClassA obj) {
obj.value = 10;
ClassA objB = new ClassA();
objB.value = 7;
obj = objB;
}

public static void main(String args[]) {
ClassA objA = new ClassA();
objA.value = 5;
foo(objA);
System.out.println("value of objA is " + objA.value)
}
}

在這個例子中,foo引用了一個ClassA參數,感覺上是 Call by value。但是他最後print出來的數值卻是value of objA is 10

也就是說 Java 其實是使用 Call by reference 作為預設囉?可是奇怪的事情發生了,objA在後面明明被改成objB,可是數值卻是改成10之後的objA,非常的詭異

於是有人就稱這為 Call by sharing ,也就是該參數與變數本身都指向同一個物件,但是參數跟變數卻是不一樣的東西

沒有提供 Call by reference 的物件導向程式,大多會是使用 Call by sharing 這種形式的參數引用
也就是只要操作到 Object 或是 Object 的衍生類別,都是 Call by sharing

到頭來其實只有 Call by value 一種

雖然上面講了3種參數引用的形式,但是實際上除了 Call by value 之外的其他2種都只是在說明他們的行為

這2種形式都是建立在 Call by value 之上,也就是說,他們實際上都還是傳遞純數值,但是他的行為可能會從單純的數值引用變成變數引用

講起來可能有點抽象,所以我決定拿出剛剛在 Call by reference 的例子來說明:

void add(int *a, int *b) {
*a = *a + *b;
}

int main() {
int a = 5;
int b = 6;
add(&a, &b); // 應該要是 5+6=11
print("a = %d", a); // 這邊印出的是 a = 11
return 0;
}

還記得這個這個例子吧?

其實我上面也有說過,它其實傳遞的是變數的位址

也就是說其實這個ab都還是普通的變數,但是因為他們的數值是位址,所以你在操作時可以依據這個位址去修改這個位址上的數值

各位可以試著在add函式的最後面加上a = 7這句,你會發現最後a還是沒有變成7,也就是說單純修改a是不會有變化的,但是修改*a就不是這麼一回事了

因為*a是操作a這個位址上的數值,跟直接修改a數值是不一樣的

這其實就是 Call by sharing 所解釋的現象,從 java 所產生出來的物件,在丟給變數的時候,其實只是將這個物件的位址丟給變數,而呼叫函式時時僅僅只會傳遞位址,直接修改參數也只是改變他的參考位址而已

結語

到這裡不知道各位能不能接受這種說法呢?

我覺得都以 Call by value 說明會讓各位更清楚 Call by reference 跟 Call by sharing 這兩種形式的運作方式

希望透過這種方式來講述他們的運作原理,可以讓各位更了解使用這些形式的參數會發生什麼事

由於很多新手會認為這幾種是完全不同的東西,因此在發生某些神奇現象時常常會滿頭問號(這邊指的就是直接對參數進行修改,但是原本的變數數值卻沒有改變的情況,通常發生在C語言)

如果這篇文有什麼錯誤的地方,或是有其他問題的,可以透過下面的 gitalk 告訴我呦~