按值传递还是指针传递?

变量赋值有两种方式:按值传递、按”指针”传递(指针也常称为”引用”)。不同的编程语言赋值的方式不一样,例如Python是按”指针”传递的,Go是按值传递的。

注意,”指针”加了引号,因为它不是真正的按指针拷贝,见下文分析。

参数传值其实也是变量赋值的过程,只不过参数是函数的本地变量而已。

按值传递的意思是每次赋值都拷贝内存中完整的数据结构对象,这时在内存中会保存两份内容完全相同,但地址不同的数据对象。

按”指针”传递的意思是每次赋值都只拷贝内存中数据结构对象的地址,这个地址占用一个机器字长(一个机器字长,在32位cpu上为32bit共4字节,64位则64bit共8字节),当然有些数据结构除了指针还包括其它属性,这时可能会占用数个机器字长。总之,按”指针”传递时,由于只拷贝一份能表示数据对象的属性(比如地址),拷贝的内容非常少,速度非常快。但必须注意,拷贝”指针”后,内存中只有一份数据对象,但将有两份完全相同的指向内存中数据对象的”指针”,无论是通过哪个”指针”去修改数据对象,都会影响另一个。

对于那些不支持操作指针的语言,通常会将按”指针”传递称为”浅拷贝(shallow copy)”,然后额外提供一个函数或工具实现按指传递,这称为”深拷贝(deep copy)”。

例如:

1
2
a=10
b=a

首先会在内存中划分一个格子用来创建数据对象10,然后将这个数据对象的地址保存到变量a中。

如果是按值拷贝的语言,则会在内存中拷贝一份数据对象10的副本,再将这个副本数据对象的地址保存到b中。

显然,a和b保存的地址是不一样的,内存中也有两份内容完全相同的数据对象10。所以,修改a的值时不会影响b的值,修改b的值时不会影响a。

如果是按”指针”拷贝的语言,则会直接拷贝a中的地址并保存到b中。

因为a和b的地址都一样,所以,修改a的值会影响b,修改b的值会影响a。

也许你已经发现了,按”指针”传递时,虽然a、b保存的地址相同,但如果a=11,a将指向新的数据对象,而b仍然指向10,即b=10,修改a并没有影响b。这是因为数值是不可变的,无法在原始的内存地址处修改,也就是无法将10替换成11,所以只要想修改这种不可变的对象就一定会创建新数据对象。对此,有两方面需要说明。

一方面,有些数据对象是可以在原始内存地址处直接进行替换修改的(例如python中的列表)。假设,某编程语言对数值也是可原处修改的,那么a=11将会在内存中将10替换成11,而不会新创建另一个数据对象11。

另一方面,上面的”按指针传递”并非是真正的按指针传递,而是按引用传递,或者说是按地址传递。这就是前文”按指针传递”中的”指针”都加上了引号的原因。

真正的指针是额外保存的,是占用空间的,和变量不同(变量保存了地址,在栈空间中),它是保存在堆内存中的。对于支持指针操作的语言(如C、C++、Go等),需要使用语法独立生成数据对象的指针,这类语言一般都能直接在原处修改数据对象。例如:

1
2
a=10
b=&a

其中b=&a表示生成a所指向(因为a保存了地址)数据对象的一个额外的指针,这个指针中保存了数据对象的地址,然后将这个指针赋值给b,这时b保存的是指针的地址,而不是数据对象的地址。

这时,修改a,或者修改b都会影响另一方,因为支持指针操作的语言一般都支持原处修改:

1
2
a=11
print(*b) /* 输出11 */

其中*b表示解除指针的引用,也就是取得数据对象的内容。

再回到按”指针”传递的拷贝方式,虽然它不是真正的拷贝指针,而是拷贝地址,但对于那些支持原处修改的数据对象,它们达到的效果和真实的指针传递是一样的。例如,数组、python的列表。

1
2
3
4
5
# 以下为python代码:
L1=[1,2,3,4]
L2=L1
L2[0]=11
print(L1) # 输出:[11,2,3,4]

可变对象的原处修改

支持指针操作的语言,通过指针修改数据时,是直接在原始地址块上修改为新数据的。例如:

1
2
3
4
5
6
7
8
9
func main() {
a := 10
println(a)
println(&a)
println("---------------")
*(&a) = 20
println(a)
println(&a)
}

结果:

1
2
3
4
5
10
0xc042085f48
---------------
20
0xc042085f48

但是python中的可变对象(比如列表),虽然俗称”原处修改”,但并非真的原处修改,而是在堆内存中新创建一个数据对象,并将它作为可变对象的一部分,所以可变对象整体的地址没有改变,但内部元素的地址已经改变了,也就是旧的元素对象被回收。

1
2
3
4
5
6
>>> L=[222,333,444,555]
>>> id(L),id(L[1])
(44652184, 43798256)
>>> L[1]=3333
>>> id(L),id(L[1])
(44652184, 43798240)