引數與參數


在許多 C++ 文件中都會談到,呼叫函式時會有傳值(Pass by value)、傳參(Pass by reference)之別,不過這兩個名詞並沒有嚴謹的定義,後續有些語言在討論函式呼叫引數與參數之間的關係時,也常不嚴謹或自顧自地使用這兩個名詞,造成了開發者之間的溝通誤會,我個人是不建議使用傳值、傳參來描述引數與參數間的關係。

在呼叫函式時,提供給函式的資料稱為引數(argument),接受引數的稱為參數(parameter),引數與參數之間的關係,其實就像是指定運算子 = 右側運算式與左側變數之間的關係,變數宣告時可以怎麼宣告,參數基本上就可以怎麼宣告,根據變數的型態而決定如何儲存、參考物件,參數就會有同樣的行為。

例如以下的範例,參數 nint 型態,呼叫函式時提供 x 作為引數:

#include <iostream> 
using namespace std; 

int increment(int n) {
    n = n + 1;
    return n;
}

int main() {
    int x = 10;
    cout << increment(x) << endl;
    cout << x << endl;

    return 0;
}

可以想成呼叫函式時,執行了 int n = x 這個動作,然後執行函式的內容,當然地,n 雖然作了遞增運算,但是對 x 的儲存值沒有影響,x 最後仍是顯示 10。

對於底下這個範例:

#include <iostream> 
using namespace std; 

int increment(int *n) {
    *n = *n + 1;
    return *n;
}

int main() {
    int x = 10;
    cout << increment(&x) << endl;
    cout << x << endl;

    return 0;
}

可以想成呼叫函式時,執行了 int *n = &x 這個動作,因此 *n 提取出來的就是 x,對 *n 的設值,就是對 x 的設值,因此程式執行後顯示的就會是 11,這跟之前談指標時的行為是一致的。

在許多 C++ 文件中,會稱以上兩個範例的函式呼叫在引數傳遞時的行為是傳值,然而,因為名詞本身沒有嚴謹定義,不建議使用這名詞來溝通。

會想要在參數上使用指標的原因很多,像是基於效率不想傳遞整個物件,考慮傳遞位址比較經濟的情況,或者是要傳遞的引數確實就是指標,例如在〈字元陣列與字串〉中談到的 C 風格字串,本質上是字元陣列,透過陣列名稱會取得首元素位址,函式若要接受這類字串,可以使用 char* 型態的參數:

#include <iostream> 
using namespace std; 

void foo(char *s) {
    cout << s << endl;
}

int main() {
    char name[] = "Justin";
    foo(name);
    return 0;
}

至於底下的範例:

#include <iostream> 
using namespace std; 

int increment(int &n) {
    n = n + 1;
    return n;
}

int main() {
    int x = 10;
    cout << increment(x) << endl;
    cout << x << endl;

    return 0;
}

可以想成呼叫函式時,執行了 int &n = x 這個動作,因此 n 就是 x 的別名,對 n 的設值,就是對 x 的設值,因此程式執行後顯示的就會是 11,這跟之前談參考時的行為是一致的。

在許多 C++ 文件中,會稱以上的函式呼叫在引數傳遞時的行為是傳參,然而,因為名詞本身沒有嚴謹定義,不建議使用這名詞來溝通。

會想在參數上使用參考的原因也有許多,通常是基於效率,直接令參數就是物件的別名(連位址都不用傳遞)。

在這邊要回顧一下〈rvalue 參考〉中談到的,為何會需要使用 const int &r = 10 這種語法,因為 lvalue 參考不能直接參考字面常量,底下範例會編譯失敗:

#include <iostream> 
using namespace std; 

int foo(int &n) {
    return n + 1;
}

int main() {
    int x = 10;
    foo(x);
    foo(10);   // error: cannot bind non-const lvalue reference of type 'int&' to an rvalue of type 'int'
    return 0;
}

若要 foo 呼叫都能通過編譯,foo 的參數必須以 const int &n 來宣告:

#include <iostream> 
using namespace std; 

int foo(const int &n) {
    return n + 1;
}

int main() {
    int x = 10;
    foo(x);
    foo(10);  // OK
    return 0;
}

C++ 11 開始可以使用 rvalue 參考,參數也可以宣告 rvalue 參考,當兩個函式各定義了 rvalue 參考與 const 的 lvalue 參考作為參數,使用常量呼叫時,編譯器會選擇 rvalue 參考的版本:

#include <iostream> 
using namespace std; 

void foo(int &&n) {
   cout << "rvalue ref" << endl;
}

void foo(const int &n) {
   cout << "lvalue ref" << endl;
}

int main() {
    foo(10);  // 顯示 rvalue ref
    return 0;
}

參數以 rvalue 參考宣告的情況,主要考慮的是效率,在函式內容的實作上往往也就有別於 const 的 lvalue 參考之版本,例如搭配 std::move 來實現移動語義(move semantics),這之後再來討論。