最近在学C++,在刷题的时候遇到了关于引用、指针的问题,重温相关知识后发现C++中的引用于python中引用有很大的区别,我想这就是C++效率远高于python的原因之一。通过两篇文章想梳理下python与C++在引用上的区别以及C++中引用和指针的区别。

Python的引用

在python中引用就是引用赋值,等同于浅拷贝,可以看一个例子:

In [13]: a = 1

In [14]: b = a

In [15]: id(a)
Out[15]: 4553065616

In [16]: id(b)
Out[16]: 4553065616

In [20]: b is a
Out[20]: True

In [21]: b == a
Out[21]: True

上面代码中先初始化了一个值为1,名字为a的变量(其实python没有变量,正确说法是标签),因为它无需声明变量类型,之后变量b引用了标签a,这个时候标签b的值等同于标签a的值并且它们的内存地址也是相同的。

WechatIMG89.png

但是python中引用还有另外一个特性:可以对应用进行更改

In [23]: b = 2

In [24]: id(b)
Out[24]: 4553065648

In [25]: a
Out[25]: 1

In [26]: id(a)
Out[26]: 4553065616
---------------------
In [24]: b=b+1

In [25]: b
Out[25]: 2

In [26]: a
Out[26]: 1

In [27]: id(a)
Out[27]: 4369581200

In [28]: id(b)
Out[28]: 4369581232

这个时候变量b已经不再引用变量a了,而是引用了2这么一个int数据,并且它的内存地址也不再与a相同。再回过头来看a,发现a并没有被b给影响到。这里其实要细说的话可以归功于在python中int数据类型是不可变数据类型,所以在变量b被重新引用后没有将a的值以及内存做改变,也就是说int是一个线程安全的数据类型。
PS
python在引用上与C++最大的区别就是:C++的引用一旦被赋值之后将不能改变引用对象,接下来的赋值操作都只会修改引用对象本身。
而python不一样,它没有这个概念,引用被赋值后它会把标签挂到最新的对象上。

可变类型数据
接下来再看下python中对于可变类型数据的引用是怎么样的

In [1]: a = [1,2,3]
In [2]: b = a

In [4]: b.append(4)

In [5]: b
Out[5]: [1, 2, 3, 4]

In [6]: id(b)
Out[6]: 4415640192

In [7]: id(a)
Out[7]: 4415640192

In [8]: a
Out[8]: [1, 2, 3, 4]

对于可变类型数据,标签b先引用了标签a所引用的对象。这个时候我们去操作标签b对数据进行更改,会发现标签a所引用的对象也受到了改变。

09522792-1FDB-4F5F-8C96-45E4EA37F4B7.png
34981B25-331C-454F-AFF8-7EF1856965B1.png
在对可变类型数据执行更改操作的时候,它们操作的本身其实就是引用的对象。所以不管是a还是b做了更改操作都会影响到该对象的其他引用。

如果对可变类型数据做重新赋值操作

In [12]: a=[1,2,3]

In [13]: b=a

In [14]: b=[2,3,4]

In [15]: a
Out[15]: [1, 2, 3]

In [16]: id(a)
Out[16]: 4406584576

In [17]: id(b)
Out[17]: 4406530608

这个时候就又会回到和不可变类型一样的情况了,标签b去重新引用了新的对象,和a说拜拜了。

09522792-1FDB-4F5F-8C96-45E4EA37F4B7.png

tT9ADH.jpg

接下来再看一个比较经典的例子
由于可变数据的更改操作其实就是更改它们的引用,会有这种情况出现

In [37]: a = [1,2,3]

In [38]: a[1] = a

In [39]: a
Out[39]: [1, [...], 3]

我把a[1]做了更改操作,使其指向了标签a引用的对象。本以为结果是[1,[1,2,3],3],由于更改操作只会修改引用对象本身, 列表[1,?,3]中的?指向了a本身从而导致了循环

tTVggI.jpg

要避免这种情况可以通过复制来解决这个问题。

In [41]: a = [1,2,3]

In [42]: a[1] = copy.copy(a)

In [43]: a
Out[43]: [1, [1, 2, 3], 3]

浅拷贝

对于不可变类型,浅拷贝与赋值没有区别

In [49]: a=1

In [50]: b = copy.copy(a)

In [51]: b
Out[51]: 1

In [52]: id(a)
Out[52]: 4369581200

In [53]: id(b)
Out[53]: 4369581200

标签b与标签a都将指向该数据。

再来看上面那道造成循环的题,在这儿我们可以通过拷贝来解决。

In [1]: a = [1,2,3]

In [2]: a[1] = a[:]

In [3]: a
Out[3]: [1, [1, 2, 3], 3]

在这里我们用切片作为拷贝操作,随后将a引用的对象中的第二个元素指向我们复制来的新对象。

t7c66e.jpg
t7cR0A.jpg

浅拷贝的缺陷
某天无意中发现了一个浅拷贝的缺点,很有意思。

In [31]: a=[1,[2,3],4]

In [32]: b=a[:]

In [33]: id(a[1])
Out[33]: 4436979376

In [34]: id(b[1])
Out[34]: 4436979376

In [40]: a[1][1] = 2

In [41]: a
Out[41]: [1, [2, 2], 4]

In [42]: b
Out[42]: [1, [2, 2], 4]

前面我们总结了如果浅拷贝可变数据的话,编译器会重新开辟一块内存来存储新对象,也就是说可变数据拷贝出来的新对象与原对象不是同一个对象。但是在上面例子的情况中,不可变数据作为不可变数据对象的元素存在时,拷贝会将它视作不可变对象来处理,所以我们会看到上面a[1]与b[1]内存地址相同的情况。

t7RPoR.jpg

要解决这个问题我们就必须要使用深拷贝了

深拷贝

In [43]: a=[1,[2,3],4]

In [44]: b = copy.deepcopy(a)

In [45]: a[1][1]=2

In [46]: a
Out[46]: [1, [2, 2], 4]

In [47]: b
Out[47]: [1, [2, 3], 4]

通过深拷贝来为嵌套元素开辟新的内存。
t7RBYq.jpg