參數傳遞方式

tags: 讀書會

主要分為三種:

  • call by value
    複製 原有變數的值 到新的變數上。
  • call by reference
    引用參考到 新的變數 上,修改或賦予新值都會改變 原有變數的值。
  • call by sharing
    複製參考到新的變數上,修改會改變原有變數,但賦予新值則會產生新的參考。

用實例分析 JS 屬於哪種

function swap(a, b) { var temp = a; a = b; b = temp; } var x = 10; var y = 20; swap(x, y); console.log(x, y) // 10, 20

執行 swap 後可以發現 x , y 並沒有被改變
可以得知 x , y 在做為參數傳進 swap 時,並不是真的把 x , y 傳進去,而是他們的拷貝而已。

所以改變 a , b 時不會影響 x ,y

這種把值拷貝的做法就是 call by value

在傳遞 基本型別 時要注意到一點

All primitives are immutable, i.e., they cannot be altered.
A primitive can be replaced, but it can't be directly altered.

JS 對於 基本型別 來說是不可能發生 call by reference 的
因為絕對不可能透過 function 內的引數去改變 function 外面的變數。

所以還不能我們還不能確定 JS 就是 call by value
因為還有 object , array 還沒測試!

function add(obj) { obj.number++ } var o = {number: 10} add(o) console.log(o.number) // 11

喔?居然在 function 裡面成功改變外面的東西了,但還不能確定這就是 call by reference 或 sharing。

來試試 重新賦予新的值 或發生什麼:

function add(obj) { // 讓 obj 變成一個新的 object obj = { number: obj.number + 1 } } var o = {number: 10} add(o) console.log(o.number) // 10

如果是 call by reference ,在 function 裡面把 obj 的值改掉了,外面的 o 也會一起被改掉,變成那個新的 object

而 call by sharing 基本上行為跟 reference 很像,差異在於在 function 裡面把 obj 重新賦值, obj 會重新指向一個新的 object 的記憶體位置,所以外面的 o 依舊還是原來的值。

結論就是:「在 JavaScript,primitive types 是 call by value,object 是 call by sharing」?

有一派的說法是 JavaScript 只有 call by value

在宣告一個 object 的時候,在底層實作上,其實這個 object 存的是一個記憶體位置。

將 object 傳遞給 function 時是將 記憶體位置 做為值傳進去的。

但老實說 這套用到 call by sharing 跟 call by reference也都通

因為以底層來說兩者都是傳記憶體位置進去。

那到底是哪一種?

ECMA 聖經 完全沒有說

不論是 ECMA-262 edition 8 或是 ECMA-262 第一版都沒有明確的說 JS 到底是用何種傳遞方式。

C ++

pointer

指標可指向特定的記憶體位址,而不直接操作變數或物件

type* ptr; //每一個指標都有一個相對應的型態 int* iptr; float* fptr; char* cptr;

形態的指標差異在於說,今天要對記憶體位置做修改時,像是記憶體位置 +1 , 其差異就會差一個型別這麼多。

指標擁有兩種操作特性

  • 操作指標所儲存的位址
  • 操作指標所指向位址之資料
    使用提取 (Dereference)運算子 * 來提取指標所指向位址的資料
int var = 10; 
int *ptr = &var;

cout << "指標ptr儲存的值:" << ptr //0x22ff74
     << endl; 
cout << "取出ptr指向的記憶體位置之值:" << *ptr //10
     << endl;

reference

在c++底層中,reference 是通過 pointer實現的。也就是說,在實現層面上,reference 就是 pointer。

直接取得變數或物件的位址,並間接透過參考型態別名來操作物件

區別在於 :

  • reference 代表了變數或物件的一個別名( alias ),本身沒有自己的記憶體位置

    ​​​​int i = 10;
    ​​​​int* ptr_i = &i;
    ​​​​int& ref_i = i;
    ​​​​
    ​​​​cout<< "i\t" << i << "\t&i\t"<< &i << endl;
    ​​​​cout<< "*ptr_i\t" << *ptr_i << "\tptr_i\t"<< ptr_i << "\t&ptr_i\t"<<&ptr_i<<endl;
    ​​​​cout<< "ref_i\t" << ref_i << "\t&ref_i\t"<< &ref_i << endl;
    

  • pointer 可以為空,雖然很危險!

    ​​​​int* ptr_i;
    
  • reference 不可為空,編譯會報錯!

    ​​​​int& ref_i;
    ​​​​//error: ‘ref_i’ declared as reference but not initialized
    
  • reference 一旦確定就不可被修改 , pointer 可更改指向其他記憶體位置
    reference

    ​​​​#include <iostream>
    ​​​​#include <string>
    ​​​​using namespace std; 
    
    ​​​​int main()
    ​​​​{
    ​​​​    int i = 10;
    ​​​​    int j = 20;
    ​​​​    int& ref_i = i;
    ​​​​    //i:10 , j:20 , ref_i:10
    ​​​​    cout<< "ref_i\t" << ref_i << "\t&ref_i  "<< &ref_i << endl;
    ​​​​    cout<< "i\t" << i << "\t&i\t"<< &i << endl;
    ​​​​    cout<< "j\t" << j << "\t&j\t"<< &j << endl<<endl;
    
    ​​​​    ref_i = j;
    ​​​​    //i:20 , j:20 , ref_i:20
    ​​​​    cout<< "ref_i\t" << ref_i << "\t&ref_i  "<< &ref_i << endl;
    ​​​​    cout<< "i\t" << i << "\t&i\t"<< &i << endl;
    ​​​​    cout<< "j\t" << j << "\t&j\t"<< &j << endl;
    ​​​​    return 0; 
    ​​​​}
    

    pointer

    ​​​​#include <iostream>
    ​​​​#include <string>
    ​​​​using namespace std; 
    
    ​​​​int main()
    ​​​​{
    ​​​​    int i = 10;
    ​​​​    int j = 20;
    ​​​​    int* ptr_i = &i;
    ​​​​    //i:10 , j:20 , *ptr_i:10
    ​​​​    cout<< "*ptr_i\t" << *ptr_i << "\tptr_i\t"<< ptr_i << endl;
    ​​​​    cout<< "i\t" << i << "\t&i\t"<< &i << endl;
    ​​​​    cout<< "j\t" << j << "\t&j\t"<< &j << endl<<endl;
    ​​​​    //i:10 , j:20 , *ptr_i:20
    ​​​​    ptr_i = &j;
    ​​​​    cout<< "*ptr_i\t" << *ptr_i << "\tptr_i\t"<< ptr_i << endl;
    ​​​​    cout<< "i\t" << i << "\t&i\t"<< &i << endl;
    ​​​​    cout<< "j\t" << j << "\t&j\t"<< &j << endl;
    ​​​​}
    

很少會直接如上的方式來使用參考,而是用於函式傳遞時一種「傳參考」(Pass by reference)方式,目的在於可於函式中直接操作目標變數或物件,或者是避免複製一個大型物件。

void swap(int &a, int &b) {
  
  // 印出 a 跟 b 所存的值與記憶體位置
  cout<< "a\t" << a << "\t\tb\t"<<b<<endl;
  cout<< "&a\t" << &a << "\t&b\t"<<&b<<endl;
  int temp = b;
  b = a;
  a = temp;
}
  
int main(){
  int x = 10;
  int y = 20;
  
  // 印出 x 跟 y 的記憶體位置
  
  cout<< "&x\t" << &x << "\t&y\t"<<&y<<endl;
  swap(x, y); // 傳記憶體位置進去
  cout<< "x\t" << x << "\t\ty\t"<<y<<endl;
}

就像上面 call by reference 舉例的那段程式碼一樣,x的記憶體位置跟a一樣,y的記憶體位置跟b一樣,因此你可以說他們兩者是「一模一樣」的東西。

可是在 call by value 的範例中,就算你傳的是指標好了,只有「指標裡面存的值(也就是指到的記憶體位置)」是一樣的,但指標本身還是有不同的記憶體位置。

void swap(int *a, int *b) {
  
  // 印出 a 跟 b 所存的值
  cout<< "a\t" << a << "\tb\t"<<b<<endl;
  cout<< "&a\t" << &a << "\t&b\t"<<&b<<endl;
  int temp = *b;
  *b = *a;
  *a = temp;
}
  
int main(){
  int x = 10;
  int y = 20;

  cout<< "x\t" << x << "\t\ty\t"<<y<<endl;
  cout<< "&x\t" << &x << "\t&y\t"<<&y<<endl;
  swap(&x, &y); // 傳記憶體位置進去
  cout<< "x\t" << x << "\t\ty\t"<<y<<endl;

}

換句話說,在 call by value 的時候我們是「新建了一個變數a,並且讓a存的值跟傳進來的參數一樣」。

所以 call by value 會把傳進去的值複製(無論那個值是數字也好,記憶體位置也好,都會複製一份)

而在 call by reference 的時候,我們只是「讓a作為x的 alias,兩個是同樣的變數」,這也許是兩間之間最大的差異。

但 call by reference 在「最底層的實作」上當然也會有類似的行為,畢竟 reference 是通過 pointer實現的,但是你感覺不出來,因為 C++ 已經幫你處理掉了。

結論: 從「行為」上面來判別到底是屬於哪一種

每個人都對 call by reference 以及 call by value 的「定義」其實都不盡相同,而且也沒有一個權威性的出處能夠保證這個定義是正確的。

要說 call by reference 及 call by value 其實都是 call by value 也沒錯,畢竟妳是以 把 reference 當做 value 傳進 function 做為出發點思考的。

所以在該語言在其自身沒有表明自己是何種方式情況下,與其不斷探究,不如從行為上來思考會比較合乎邏輯。

有關技術名詞的解釋,在 技術名詞紛爭多 這本書中一段話說道:

程式開發的世界中,名詞的創造經常是隨意的,曾經在 Java 中爭執不斷的考古題之一是:「Java 中有沒有 Pass by reference」,就現今來說,大家公認的答案是沒有,Java 只有 Pass by value,不過還是有人面對 Java 文件中經常出現 reference,而搞不清楚。

說穿了,這個名詞與 C++ 中的 reference 定義不同,只不過 Java 最初不知道為什麼,也用了 reference 一詞,重點也不在搞清楚 Pass by value,重點是搞清楚透過參數操作物件時,會有什麼樣的行為。

面幾句話都是正確的:

  • JavaScript 裡面只有 pass by value
  • JavaScript 的 primitive type 是 pass by value,object 是 pass by sharing

參考資料

深入探討 JavaScript 中的參數傳遞:call by value 還是 reference?

詳解c++ 引用(reference)與指針(pointer)的區別與聯繫
JavaScript - 參數傳遞方式 (2)

指標與記憶體位址

參考(reference)

Select a repo