基本类型的「值拷贝」与「引用拷贝」
对象的状态由基本类型定义而来,搞清基本类型的两种拷贝(传递)方式将有助于推导出对象的传递方式。
值拷贝
对于原有的基本类型数据,值拷贝产生一个新的数据。此时新数据与原数据的数值相同,但两者是不同的两个数据,改变其中一个数据的值不会影响另一个数据的值。
引用拷贝
引用拷贝不会产生新的数据,而是添加一个对原有数据的访问标识。此时新标识与原有标识指代的是同一个数据,从一个标识改变数据,再从另一个标识访问数据则会得到改变后的数据,因为两个标识指向的是同一个数据。
对象的「深拷贝」与「浅拷贝」
此处把对象看作是以基本类型为叶节点的树:要拷贝一颗树,只需依次拷贝子节点。对于子节点的拷贝,仍然可以借用值拷贝与引用拷贝的概念:
- 引用拷贝并不需要了解自己到底拷贝了什么,毕竟对象就在那里,引用不过是对象的另一个标识;
- 值拷贝就比较麻烦了,我们发现一个尴尬的问题:要明白如何拷贝对象,就需要明白如何拷贝子节点,如果子节点是对象,那问题又回来了……
好在,子节点是有边界的——引用拷贝本身就可以结束;值拷贝虽然套娃,但一层一层套下去,总能遇见基本类型数据——对此我们是了解的。于是,对象的拷贝可以描述成一种递归:
1 2 3 4 5 6 7 8 9
| 拷贝对象(原对象): 遍历原对象中的子节点: if 引用拷贝: 直接引用原有子节点 else: if 子节点是基本类型数据: 值拷贝 else: 拷贝对象(子节点)
|
于是,我们用利用值拷贝和引用拷贝严格地描述了对象的拷贝。实际情况中常常用「深拷贝」和「浅拷贝」来描述:
深拷贝
对于原有对象,每一层的子节点都按值拷贝,深度优先搜索整个对象,遇到基本数据则拷贝并退回。
浅拷贝
对于原有对象,直属的子节点都按其默认方式拷贝。
注意区分:引用对象、浅拷贝和深拷贝。引用是绝对的浅,深拷贝则是绝对的深,唯独浅拷贝的「浅」是有歧义的,某些情况下两个不同的拷贝方法会产生同样的结果。
浅拷贝和深拷贝可能不足以描述所有的情况,只需要记住,值拷贝和引用拷贝就能够严格地描述对象的拷贝。
不同语言的拷贝规则
C
基本类型
通过=
(赋值)或函数间参数传递的拷贝都是值拷贝。
1 2 3 4 5 6 7 8 9 10 11 12
| void args_in_func(int arg, int *ptr) { printf("argument in function:%p\n" "pointer in function:%p\n", &arg, ptr); }
int mian(void) { int ori = 1; int copy = ori; printf("ori:%p\ncopy:%p",&ori, ©); args_in_func(ori, &ori); return 0; }
|
结果:
1 2
| ori: 000000f30c5ff63c copy: 000000f30c5ff638
|
说明 copy
是与原数据有相同值的另一个数据。
指针
指针是一类特别的数据,用于存放其他数据的地址。C 中不直接提供引用,而是通过指针间接达到引用的效果。
就指针本身而言,赋值和传递都是关于地址的值拷贝;就指针指向的数据而言,则是关于数据的引用拷贝。
1 2 3 4 5 6 7 8 9 10 11 12
| void args_in_func(int arg, int *ptr) { printf("argument in function:%p\n" "pointer in function:%p\n", &arg, ptr); }
int main(void) { int ori = 1; int copy = ori; printf("ori:%p\ncopy:%p\n",&ori, ©); args_in_func(ori, &ori); return 0; }
|
结果:
1 2 3 4
| ori: 0000001fe19ffc7c copy: 0000001fe19ffc78 argument in function: 0000001fe19ffc50 pointer in function: 0000001fe19ffc7c
|
函数中的参数和原参数是不同的两个变量。对于指针本身也是如此,不过指针保存的地址相同,即可以通过不同的指针访问同一个变量。
结构体
结构体默认是将成员变量(按照默认拷贝方式)依次拷贝一遍:基本类型数据是值拷贝,指针是拷贝地址,结构体则拷贝成员。
需要注意,如果原结构体中的指针指向了某个不希望被共用的数据,应当深拷贝这份数据,再用新结构体中的指针指向原数据的深拷贝。
C++
引用
1 2 3 4 5 6 7 8 9 10 11
| void refer_in_func(int &refer) { printf("refer in function:%p\n", &refer); }
int main() { int ori = 1; int &refer = ori; printf("ori:%p\nrefer:%p\n", &ori, &refer); refer_in_func(ori); return 0; }
|
结果:
1 2 3
| ori: 000000fe451ff9d4 refer: 000000fe451ff9d4 refer in function: 000000fe451ff9d4
|
引用来引用去,到底是一个东西。
类
C++ 中类的默认赋值运算符 =
与 C 中的结构体类似:依次复制成员变量,遇到指针不跳转。
1 2 3 4 5 6 7 8 9 10 11 12
| class Num{ public: int num{}; };
int main() { Num obj_a; Num obj_b = obj_a; obj_b.num = 1; printf("obj_a:%p\nobj_b:%p\n", &obj_a, &obj_b); printf("obj_a.num:%p\nobj_b.num:%p\n", &obj_a.num, &obj_b.num); }
|
结果:
1 2 3 4
| obj_a:000000aadc3ffa6c obj_b:000000aadc3ffa68 obj_a.num:000000aadc3ffa6c obj_b.num:000000aadc3ffa68
|
也许更常见的是自定义的拷贝:重载 =
运算符或者重载一个构造方法。
此外,对象在函数间的传递不值得额外考虑,因为如果要改变对象就直接引用;不改变对象 const &
也好处颇多,即使需要副本也可以在函数内部实现。
Java
Java 中的赋值运算:基本数据类型的都是值拷贝,对象都是引用拷贝。
基本类型
1 2 3 4 5 6 7 8 9
| public class Test { public static void main(String[] args) { int num = 0; System.out.println("num:" + num); int copy = num; copy = 1; System.out.println("change copy:" + copy + "\nnum:" + num); } }
|
结果:
1 2 3
| num:0 change copy:1 num:0
|
对象
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
| public class Num { public int num;
public Num(int num) { this.num = num; }
public Num() { this(0); }
public static void change(Num aNum) { aNum.num = 2; System.out.println("change in func:" + aNum.num); }
public static void main(String[] args) { Num aNum = new Num(); Num bNum = new Num(); bNum = aNum; bNum.num = 1; System.out.println("\naNum.num:" + aNum.num); Num.change(aNum); System.out.println("change in outside:" + aNum.num); } }
|
结果:
1 2 3
| aNum.num:1 change in func:2 change in outside:2
|
Java 虽然不能直接看到内存地址,但通过结果可以发现:
- 赋值运算改变基本类型的原有数据;
- 赋值运算改变对象的指向:此处
bNum
虽然 new
了一个对象,但赋值的操作并不是改变这个对象,而是抛弃这个对象。
Python
Python 有一个重要的概念,叫作「一切皆对象」。就对象而言,和 Java 一样赋值运算都是引用拷贝。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17
| import copy
a = [1, 2, 3, [4, 5]] assignment = a cut = a[:] factory = list(a) copy_deepcopy = copy.deepcopy(a) copy_copy = copy.copy(a)
a[0] = 0 a[3][0] = 0 print("a:{} {}".format(a, id(a))) print("assignment:{} {}".format(assignment, id(assignment))) print("cut:{} {}".format(cut, id(cut))) print("factory:{} {}".format(factory, id(factory))) print("copy_copy:{} {}".format(copy_copy, id(copy_copy))) print("copy_deepcopy:{} {}".format(copy_deepcopy, id(copy_deepcopy)))
|
结果:
1 2 3 4 5 6
| a: [0, 2, 3, [0, 5]] 2291611479552 assignment: [0, 2, 3, [0, 5]] 2291611479552 cut: [1, 2, 3, [0, 5]] 2291611724224 factory: [1, 2, 3, [0, 5]] 2293335453312 copy_copy: [1, 2, 3, [0, 5]] 2291611723712 copy_deepcopy: [1, 2, 3, [4, 5]] 2291611724480
|
可以看到,默认的赋值运算是直接引用,而切片方法、工厂方法和 copy
模块中的 copy()
方法则是浅拷贝,只有copy
模块中的 deepcopy()
方法是深拷贝。