求值策略,值传递,引用传递讲解

求值策略通常指对某种编程语言的表达式进行求值和计算的一个规则集

而函数参数的传值策略是其中一个特殊的例子

  • 所有这些实参的形式,都统称为表达式(Expression)
  • 求值(Evaluation)即是指对这些表达式的简化并求解其值的过程

求值策略的划分依据是:求值的时机(调用前还是调用中)和值本身的传递方式

求值策略求值时间传值方式
值传递(Pass by value)调用前值的结果(原值的副本)
引用传递(Pass by reference)调用前原值(原始对象,无副本)
名传递(Pass by name)调用后(用到才求值)与值无关的一个名

求值策略(值传递和引用传递)的关注的点在于,这些表达式在调用函数的过程中,求值的时机、值的形式的选取等问题 求值的时机,可以是在函数调用前,也可以是在函数调用后,由被调用者自己求值 这里所谓调用后求值,可以理解为 Lazy LoadOn Demand 的一种求值方式

在函数调用过程中,调用方提供实参

1
2
3
4
5
6
7
8
// 可以是常量
call(1);
// 也可以是变量
Call(x);
// 可以是他们的组合
Call(2 * x + 1);
// 以是对其它函数的调用
Call(GetNumber());

按值传递在传递的时候,实参被复制了一份,然后在函数体内使用 使用值传递,一般在实参本身不改变时使用

调用时,使用指针是常见的做引用传递手段,比如

1
2
3
void count(int *a int *b)
// 也可以
 void count(int& ca int& cb)

这样,在函数执行后,这些值被交换 使用引用传递的场景,肯定是传递很大的自定义对象,这样能保证传递的安全

语言层直接支持名传递的语言很不主流,在C#,名传递的行为可以用 Func<T> 来模拟

为什么不是 value(值) 而是 名传递?

  • 对于值传递,无论是值类型还是引用类型,都会在调用栈上创建一个副本
  • 不同是,对于值类型而言,这个副本就是整个原始值的复制
  • 而对于引用类型而言,由于引用类型的实例在堆中,在栈上只有它的一个引用,一般情况是一个指针
  • 引用类型所创建的副本是,这个引用的复制,而不是整个原始对象
  • 而名传递,根本不会和原值直接打交道

值传递与引用传递,在计算机领域是专有名词, 理解下面的解释时,请不要把任何概念往你所熟悉的语言功能上套, 很容易产生误解

值传递和引用传递,属于函数调用时参数的求值策略

对调用函数时,求值和传值的方式的描述,而非传递的内容的类型( 值类型 还是 引用类型,或者是 还是 指针)

值类型/引用类型 ,是用于区分两种内存分配方式

  • 值类型在调用上分配
  • 引用类型上分配

一个描述内存分配方式,一个描述参数求值策略,两者之间无任何依赖或约束关系!

区别值传递引用传递
本质区别创建副本(copy)不创建副本
导致结果函数执行中,无法改变原始对象函数执行过程可以改变对象

这里的改变不是指 mutate, 而是 change ,指把一个变量指向另一个对象,而不是指仅仅改变属性或是成员什么的

Java,所以说Java是Pass by value,原因是它调用时Copy,实参不能指向另一个对象,而不是因为被传递的东西本质上是个Value 对于Java的函数调用方式最准确的描述是:参数借由值传递方式,传递的值是个引用

值类型和引用类型(这不是在说值传递)的最大区别

  • 值类型用做参数会被复制,但是很多人误以为这个区别是值类型的特性
  • 其实这是值传递带来的效果,和值类型本身没有关系

从最终结果结果上看,值类型参数被复制

求值策略定义的是函数调用时的行为,并不对具体实现方式做要求 但是指针由于其汇编级支持的特性,成为实现引用传递方式的首选

但是纯理论上,你完全可以不用指针,比如用一个全局的参数名到对象地址的HashTable来实现引用传递,只是这样效率太低,所以根本没有哪个编程语言会这样做,当然你自己写语法糖可以玩玩

参数借由值传递方式,传递的值是个引用,这个求值方式很多语言在使用 比如: Java、Python、Ruby、JavaScript

方便区别和描述,起名 Call by sharing(求值共享)

下面的例子:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
void ByValue(int a)
{
 a = a + 1;
}

void ByRef(int& a)
{
 a = a + 1;
}

void ByPointer(int* a)
{
 *a = *a + 1;
}
int main(int argv, char** args)
{
 int v = 1;
 ByValue(v);
 ByRef(v);

 // Pass by Reference
 ByPointer(&v);

 // Pass by Value
 int* vp = &v;
 ByPointer(vp);
}

同一个函数 ByPointer,一次调用是Call by reference, 一次是Call by value

因为 ByPointer(vp); 没有改变vp,且无法改变 vp ByPointer(&v); 改变了v,从行为考虑,对于调用者而言,v的确被ByPointer函数改了

C语言不支持引用,只支持指针,但是使用指针的函数,不能通过签名明确其求值策略 C++引入了引用,它的求值策略可以确定是Pass by reference

如果观察一下 void ByRef(int& a)void ByPointer(int* a) 所生成的汇编代码,会发现在一定条件下其实是一样的

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
; 12 : {

 push	ebp
 mov	ebp, esp
 sub	esp, 192	; 000000d0H
 push	ebx
 push	esi
 push	edi
 lea	edi, DWORD PTR [ebp-192]
 mov	ecx, 48	; 00000030H
 mov	eax, -858993560	; ccccccccH
 rep stosd

; 13 : *a = *a + 1;

 mov	eax, DWORD PTR _a$[ebp]
 mov	ecx, DWORD PTR [eax]
 add	ecx, 1
 mov	edx, DWORD PTR _a$[ebp]
 mov	DWORD PTR [edx], ecx

于是C++的一个奇葩的地方来了,它语言本身(模拟的不算,什么都能模拟)支持Call by value和Call by reference两种求值策略,但是却提供了三种语法去做这个

相对 C# 的设计就相对合理,函数声明里

  • ref/out ,就是引用传递
  • 没有 ref/out ,就是值传递,与参数类型无关

当然也有语言非常简单粗暴,比如完全对象的语言 ruby ,传递的是 引用的拷贝 使用 ruby 时不用考虑值传递,而只需要提供 clone dup(浅拷贝) 和 Marshal.load(深拷贝) 来处理引用的 in/out