在許多 C++ 文件中都會談到,呼叫函式時會有傳值(Pass by value)、傳參(Pass by reference)之別,不過這兩個名詞並沒有嚴謹的定義,後續有些語言在討論函式呼叫引數與參數之間的關係時,也常不嚴謹或自顧自地使用這兩個名詞,造成了開發者之間的溝通誤會,我個人是不建議使用傳值、傳參來描述引數與參數間的關係。
在呼叫函式時,提供給函式的資料稱為引數(argument),接受引數的稱為參數(parameter),引數與參數之間的關係,其實就像是指定運算子 =
右側運算式與左側變數之間的關係,變數宣告時可以怎麼宣告,參數基本上就可以怎麼宣告,根據變數的型態而決定如何儲存、參考物件,參數就會有同樣的行為。
例如以下的範例,參數 n
是 int
型態,呼叫函式時提供 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),這之後再來討論。