实参与参数


在许多 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),这之后再来讨论。


展开阅读全文