对比 C++ 和 Python,谈谈指针与引用

点击上方蓝字“Python猫”,免费获得一个公众号

花下猫语:本文是学习群内樱雨楼小姐姐的投稿。之前已发布过她的一篇作品《当谈论迭代器时,我谈些什么?》,大受好评。本文依然是对比 C++ 与 Python,来探讨编程语言中极其重要的概念。祝大家读有所获,学有所成!

 
 
640?wx_fmt=jpeg

樱雨楼 | 原创作者

豌豆花下猫 | 编辑润色

0 引言

指针(Pointer)是 C、C++ 以及 Java、Go 等语言的一个非常核心且重要的概念,而引用(Reference)是在指针的基础上构建出的一个同样重要的概念。

指针对于任何一个编程语言而言都是必须且重要的,虽然 Python 对指针这一概念进行了刻意的模糊与限制,但指针对于 Python 而言依然是一个必须进行深入讨论的话题。

本文基于 C++ 与 Python,讨论了 Python 中与指针及引用相关的一些行为。

640?wx_fmt=gif

1 什么是指针?为什么需要指针?

指针有两重含义:

(1)指代某种数据类型的指针类型,如整形指针类型、指针指针类型

(2)指代一类存放有内存地址的变量,即指针变量

指针的这两重含义是紧密联系的:作为一种变量,通过指针可以获取某个内存地址,从而为访问此地址上的值做好了准备;作为一种类型,其决定了内存地址的正确偏移长度,其应等于当前类型的单位内存大小。

如果一个指针缺少指针类型,即 void *,则显然,其虽然保存了内存地址,但这仅仅是一个起点地址,指针会因为无法获知从起点向后进行的偏移量,从而拒绝解指针操作;而如果一个指针缺少地址,即 nullptr,则其根本无法读取特定位置的内存。

指针存在的意义主要有以下几点:

pass-by-pointer 的好处包括但不限于:

由此可见,与指针相关的各操作对于编程而言都是必须的或十分重要的。

640?wx_fmt=jpeg

2 C++中的引用

在 C++ 中,引用具有与指针相似的性质,但更加隐形与严格。C++ 的引用分为以下两种:

2.1 左值引用

左值引用于其初始化阶段绑定到左值,且不存在重新绑定。

左值引用具有与被绑定左值几乎一样的性质,其唯一的区别在于 decltype 声明:

int numA = 0, &lrefA = numA;  // Binding an lvaluecout << ++lrefA << endl;      // Use the lvalue reference as lvalue & rvaluedecltype(lrefA) numB = 1;     // Error!0, &lrefA = numA;  // Binding an lvalue
cout << ++lrefA << endl;      // Use the lvalue reference as lvalue & rvalue
decltype(lrefA) numB = 1;     // Error!

左值引用常用于 pass-by-reference:

void swap(int &numA, int &numB){    int tmpNum = numA;    numA = numB;    numB = tmpNum;}int main(){    int numA = 1, numB = 2;    swap(numA, numB);    cout << numA << endl << numB << endl;  // 2 1}
    int tmpNum = numA;
    numA = numB;
    numB = tmpNum;
}

int main()
{
    int numA = 1, numB = 2;
    swap(numA, numB);
    cout << numA << endl << numB << endl;  // 2 1
}

2.2 右值引用

右值引用于其初始化阶段绑定到右值,其常用于移动构造函数和移动赋值操作。在这些场合中,移动构造函数和移动赋值操作通过右值引用接管被移动对象。

右值引用与本文内容无关,故这里不再详述。

3 Python中的引用

3.1 Python不存在引用

由上文讨论可知,虽然“引用”对于 Python 而言是一个非常常用的术语,但这显然是不准确的——由于 Python 不存在对左/右值的绑定操作,故不存在左值引用,更不存在右值引用。

3.2 Python的指针操作

不难发现,虽然 Python 没有引用,但其变量的行为和指针的行为具有高度的相似性,这主要体现在以下方面:

由此可见,Python变量更类似于(某种残缺的)指针变量,而不是引用变量。

640?wx_fmt=jpeg

3.2.1 构造函数返回指针

对于 Python 的描述,有一句非常常见的话:“一切皆对象”。

但在这句话中,有一个很重要的事实常常被人们忽略:对象是一个值,不是一个指针或引用。

所以,这句话的准确描述应该更正为:“一切皆(某种残缺的)指针”。虽然修改后的描述很抽象,但这是更准确的。

而由于对象从构造函数而来,至此我们可知:Python的构造函数将构造匿名对象,且返回此对象的一个指针。

这是 Python 与指针的第一个重要联系。

用代码描述,对于Python代码:

sampleNum = 0

其不类似于 C++ 代码:

int sampleNum = 0;0;

而更类似于:

int __tmpNum = 0, *sampleNum = &__tmpNum;// 或者:shared_ptr<int> sampleNum(new int(0));0, *sampleNum = &__tmpNum;

// 或者:
shared_ptr<int> sampleNum(new int(0));

3.2.2 __setitems__操作将隐式解指针

Python与指针的另一个重要联系在于 Python 的隐式解指针行为。

虽然 Python 不存在显式解指针操作,但(有且仅有)__setitems__操作将进行隐式解指针,通过此方法对变量进行修改等同于通过解指针操作修改变量原值。

此种性质意味着:

  1. 任何不涉及__setitems__的操作都将成为指针重绑定。

对于Python代码:

numList = [None] * 10# RebindingnumList = [None] * 510

# Rebinding
numList = [None] * 5

其相当于:

int *numList = new int[10];// Rebindingdelete[] numList;numList = new int[5];delete[] numList;new int[10];

// Rebinding
delete[] numList;
numList = new int[5];
delete[] numList;

由此可见,对 numList 的非__setitems__操作,导致 numList 被绑定到了一个新指针上。

  1. 任何涉及__setitems__的操作都将成为解指针操作。

由于 Python 对哈希表的高度依赖,“涉及__setitems__的操作”在 Python 中实际上是一个非常广泛的行为,这主要包括:

我们用一个稍复杂的例子说明这一点:

对于以下Python代码:

class Complex(object):    def __init__(self, real = 0., imag = 0.):        self.real = real        self.imag = imag    def __repr__(self):        return '(%.2f, %.2f)' % (self.real, self.imag)def main():    complexObj = Complex(1., 2.)    complexObj.real += 1    complexObj.imag += 1    # (2.00, 3.00)    print(complexObj)if __name__ == '__main__':    main()
    def __init__(self, real = 0., imag = 0.):
        self.real = real
        self.imag = imag

    def __repr__(self):
        return '(%.2f, %.2f)' % (self.real, self.imag)

def main():
    complexObj = Complex(1.2.)
    complexObj.real += 1
    complexObj.imag += 1
    # (2.00, 3.00)
    print(complexObj)

if __name__ == '__main__':
    main()
640?wx_fmt=gif

其相当于:

class Complex{public:    double real, imag;    Complex(double _real = 0., double _imag = 0.): real(_real), imag(_imag) {}};ostream &operator<<(ostream &os, const Complex &complexObj){    return os << "(" << complexObj.real << ", " << complexObj.imag << ")";}int main(){    Complex *complexObj = new Complex(1., 2.);    complexObj->real++;    complexObj->imag++;    cout << *complexObj << endl;    delete complexObj;    return 0;}
public:
    double real, imag;
    Complex(double _real = 0.double _imag = 0.): real(_real), imag(_imag) {}
};

ostream &operator<<(ostream &os, const Complex &complexObj)
{
    return os << "(" << complexObj.real << ", " << complexObj.imag << ")";
}

int main()
{
    Complex *complexObj = new Complex(1.2.);
    complexObj->real++;
    complexObj->imag++;
    cout << *complexObj << endl;
    delete complexObj;
    return 0;
}

由此可见,无论是 int、float 这种简单的 Python 类型,还是我们自定义的类,其构造行为都类似使用 new 构造对象并返回指针。

且在 Python 中任何涉及“.”和“[]”的操作,都类似于对指针的“->”或“*”解指针操作。

4 后记

本文探讨了 Python 变量与指针、引用两大概念之间的关系,主要论证了“Python不存在引用”以及“Python变量的行为类似于某种残缺的指针”两个论点。

文中所有论点均系作者个人观点,如有错误,恭迎指正。(猫注:本文所有赞赏,归樱雨楼小姐姐所有,鼓励优质原创,支持好文章!

640?wx_fmt=jpeg

扫码赞赏她

更多优质文章,等你翻牌

1

当谈论迭代器时,我谈些什么?

2

Python对象的身份迷思:从全体公民到万物皆数

3

Python进阶:切片的误区与高级用法

4

Python 工匠:让函数返回结果的技巧

640?wx_fmt=png

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

Python猫

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值