按值传递 vs. 按指针传递
按值传递还是指针传递?
变量赋值有两种方式:按值传递、按”指针”传递(指针也常称为”引用”)。不同的编程语言赋值的方式不一样,例如Python是按”指针”传递的,Go是按值传递的。
注意,”指针”加了引号,因为它不是真正的按指针拷贝,见下文分析。
参数传值其实也是变量赋值的过程,只不过参数是函数的本地变量而已。
按值传递的意思是每次赋值都拷贝内存中完整的数据结构对象,这时在内存中会保存两份内容完全相同,但地址不同的数据对象。
按”指针”传递的意思是每次赋值都只拷贝内存中数据结构对象的地址,这个地址占用一个机器字长(一个机器字长,在32位cpu上为32bit共4字节,64位则64bit共8字节),当然有些数据结构除了指针还包括其它属性,这时可能会占用数个机器字长。总之,按”指针”传递时,由于只拷贝一份能表示数据对象的属性(比如地址),拷贝的内容非常少,速度非常快。但必须注意,拷贝”指针”后,内存中只有一份数据对象,但将有两份完全相同的指向内存中数据对象的”指针”,无论是通过哪个”指针”去修改数据对象,都会影响另一个。
对于那些不支持操作指针的语言,通常会将按”指针”传递称为”浅拷贝(shallow copy)”,然后额外提供一个函数或工具实现按指传递,这称为”深拷贝(deep copy)“。
例如:
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等),需要使用语法独立生成数据对象的指针,这类语言一般都能直接在原处修改数据对象。例如:
a=10
b=&a
其中b=&a
表示生成a所指向(因为a保存了地址)数据对象的一个额外的指针,这个指针中保存了数据对象的地址,然后将这个指针赋值给b,这时b保存的是指针的地址,而不是数据对象的地址。
这时,修改a,或者修改b都会影响另一方,因为支持指针操作的语言一般都支持原处修改:
a=11
print(*b) /* 输出11 */
其中*b
表示解除指针的引用,也就是取得数据对象的内容。
再回到按”指针”传递的拷贝方式,虽然它不是真正的拷贝指针,而是拷贝地址,但对于那些支持原处修改的数据对象,它们达到的效果和真实的指针传递是一样的。例如,数组、python的列表。
# 以下为python代码:
L1=[1,2,3,4]
L2=L1
L2[0]=11
print(L1) # 输出:[11,2,3,4]