函数参数的求值策略

函数参数的求值策略 Evaluation Strategy 指的是函数调用时,实参(表达式)的求值和传值方式,主要有两种求值策略,按值传递和按引用传递。

按值传递(pass by value)是指在调用函数时将实参复制一份传递到函数中,在函数中如果对参数进行修改,将不会影响到实参。

引用传递(pass by reference)是指在调用函数时将实参的地址直接传递到函数中,在函数中对参数所进行的修改,将影响到实参。

按值传递传递的是原始值的复制,或内存地址值的复制(比如 JS 中的共享传递,C/C++ 中的指针传递)。按引用传递传递的是内存地址(不是内存地址值)。

JavaScript 中参数的求值策略

Javascript 中函数参数求值策略是按值传递。无论是值类型还是引用类型,都会在栈上创建副本(拷贝、复制),不同是,对于值类型而言,这个副本就是整个原始值的复制,对于引用类型,由于引用类型的实例在堆中,在栈上只有它的一个地址引用值,其副本也只是这个引用值的复制,而不是整个原始对象的复制,这种策略也被称为按共享传递(传递的是地址值,可通过引用来修改原始对象的属性,重新赋值则会断开对原始对象的引用,不影响原始对象),类似于 C 中的指针传递。按共享传递是按值传递的特例。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
function change(num, obj) {
num += 10;
obj.value = 'hello'; // 只是引用类型的副作用,不能证明是引用传递

// obj = new Object(); // 断开了对 greeting 的引用
// obj.value = 'world'; // 如果是引用传递,改变形参 obj 的属性 value,也会反映在 greeting 变量中
}

const a = 10;
const greeting = {
value: 'hello world',
};
change(a, greeting);
console.log(a); // 10
console.log(greeting); // hello

上面例子的内存模型图如下:

1
2
3
4
5
6
stack(栈)         |  heap(堆)
---------------------------------
a 10 |
greeting 0x01 ---> | 0x01 hello world
num 10 |
obj 0x01 ---> |

如果是按引用传递,直接传递第二格的内容即可,不需要有第四格。

其他语言中参数的求值策略

Java 中参数求值策略与 JavaScript 一样,都是按值传递(含共享传递)。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
public class TestByValue {
public static void main(String[] args) {
int a = 10;
Greeting greeting = new Greeting();
TestByValue testByValue = new TestByValue();
testByValue.change(a, greeting);
System.out.println(a); // 10
System.out.println(greeting.value); // hello
}

public void change(int num, Greeting greeting) {
num += 10;
greeting.value = "hello";
// greeting = new Greeting(); // 断开了对 greeting 的引用
// greeting.value = "world";
}
}

class Greeting {
String value = "hello world";
}
1
2
3
4
# 编译
javac TestByValue.java
# 运行
java TestByValue

PHP 既支持值传递又支持引用传递,通过 & 运算符(取址运算符)实现引用传递。

1
2
3
4
5
6
7
8
function change(&$num) {
$num = $num + 100;
}

$a = 1;
echo $a; // 输出1
change($a);
echo $a; // 输出 101

C/C++ 支持值传递(含指针传递), 另外 C++ 还支持引用传递,通过 & 取址运算符实现引用传递。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
#include<stdio.h>

void change(int num1, int &num2, int *num3) {
num1 = 11;
num2 = 22;
*num3 = 33;

// int num4 = 2;
// num3 = &num4; // 对指针变量赋值,会断开原先的引用,不会影响外面
// printf("%p\n", num3); // 外面的 c 还是 3
}

int main() {
int a = 1;
int b = 2;
int c = 3;
change(a, b, &c);
printf("%d\n", a); // 1,值传递
printf("%d\n", b); // 22,引用传递
printf("%d\n", c); // 33 或 3,指针传递
return 0;
}

注意:上面代码需使用 gcc test.cpp -lstdc++ -o test 作为 C++ 编译,不能使用 gcc -o test test.c 作为 C 编译,C 没有按引用传递,都是按值传递,通过指针传递也可实现引用传递的效果,要想通过 C 编译,需删除引用传递。

其内存图如下。

1
2
3
4
5
a 1
b/num2 2
c [0x7ff7b2c49920] 3
num1 1
num3 [0x7ff7b2c498f8] 0x7ff7b2c49920

num1 的内容是复制于 a 的原值,num2 是 b 的别名,num3 的内容是 c 的地址值,*num3 指向 c 的内容。

指针传递本质上也是值传递的方式,它所传递的是一个地址值,与 JavaScript 中的共享传递一样。C/C++ 中通过 * 指针运算符实现指针传递。

作为指针类型数据本身,其既可使用指针传递又可使用引用传递。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
#include<iostream>

void change(int *&p) { // int *&p 表示 p 是一个整型指针的别名,int *p 则会重新分配内存,创建新指针
std::cout<<&p<<'\n';
std::cout<<p<<'\n';
std::cout<<*p<<'\n'; // 1
*p = 11;
}

int main() {
int a = 1;
std::cout<<&a<<'\n';
std::cout<<a<<'\n'; // 1
int *b = &a;
std::cout<<&b<<'\n';
std::cout<<b<<'\n';
std::cout<<*b<<'\n'; // 1
change(b);
std::cout<<a<<'\n'; // 11
return 0;
}

算子的求值策略

求值策略不但规定了函数参数的求值规则,也规定了算子的求值规则,比如赋值表达式中的 = 运算符。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
int main() {
int a = 1;
int b = 2;
int c = 3;

int num1 = a; // 值传递
int *num2; // 值传递中的指针传递
num2 = &b;
int &num3 = c; // 引用传递
num1 = 11;
*num2 = 22;
num3 = 33;

printf("%d\n", a); // 1
printf("%d\n", b); // 22
printf("%d\n", c); // 33
return 0;
}
1
2
3
4
const a = 1;
const b = {value: 2};
const num = a;
const obj = b; // 值传递中的共享传递