乐趣区

关于python:对比-C-和-Python谈谈指针与引用

0 引言
指针(Pointer)是 C、C++ 以及 Java、Go 等语言的一个十分外围且重要的概念,而援用(Reference)是在指针的根底上构建出的一个同样重要的概念。

指针对于任何一个编程语言而言都是必须且重要的,尽管 Python 对指针这一概念进行了刻意的含糊与限度,但指针对于 Python 而言仍然是一个必须进行深刻探讨的话题。

本文基于 C++ 与 Python,探讨了 Python 中与指针及援用相干的一些行为。

1 什么是指针?为什么须要指针?
指针有两重含意:

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

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

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

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

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

  • 承载通过 malloc、new、allocator 等获取的动态内存
  • 使得 pass-by-pointer 成为可能

pass-by-pointer 的益处包含但不限于:

  • 防止对实参无意义的值拷贝,大幅提高效率
  • 使得对某个变量的批改能力不局限于变量本身的作用域
  • 使得 swap、挪动构造函数、挪动赋值运算等操作能够仅针对数据结构外部的指针进行操作,从而防止了对长期对象、移后源等对象的整体内存操作

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

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

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

左值援用具备与被绑定左值简直一样的性质,其惟一的区别在于 decltype 申明:

int numA = 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
}

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

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

3 Python 中的援用
3.1 Python 不存在援用
由上文探讨可知,尽管“援用”对于 Python 而言是一个十分罕用的术语,但这显然是不精确的——因为 Python 不存在对左 / 右值的绑定操作,故不存在左值援用,更不存在右值援用。

3.2 Python 的指针操作
不难发现,尽管 Python 没有援用,但其变量的行为和指针的行为具备高度的相似性,这次要体现在以下方面:

  • 在任何状况下(包含赋值、实参传递等)均不存在显式值拷贝,当此种状况产生时,只减少了一次援用计数
  • 变量能够进行重绑定(对应于一个不含顶层 const(top-level const)的指针)
  • 在某些状况下(下文将对此问题进行具体探讨),可通过函数实参批改原值

由此可见,Python 变量更相似于(某种完好的)指针变量,而不是援用变量。

3.2.1 构造函数返回指针
对于 Python 的形容,有一句十分常见的话:“所有皆对象”。

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

所以,这句话的精确形容应该更正为:“所有皆(某种完好的)指针”。尽管批改后的形容很形象,但这是更精确的。

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

这是 Python 与指针的第一个重要分割。

用代码形容,对于 Python 代码:

sampleNum = 0

其不相似于 C++ 代码:

int sampleNum = 0;

而更相似于:

int __tmpNum = 0, *sampleNum = &__tmpNum;

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

3.2.2 __setitems__操作将隐式解指针

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

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

此种性质意味着:

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

对于 Python 代码:

numList = [None] * 10

# Rebinding
numList = [None] * 5

其相当于:

int *numList = new int[10];

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

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

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

因为 Python 对哈希表的高度依赖,“波及__setitems__的操作”在 Python 中实际上是一个十分宽泛的行为,这次要包含:

  • 对数组的索引操作
  • 对哈希表的查找操作
  • 波及__setattr__的操作(因为 Python 将 attribute 存储在哈希表中,所以__setattr__操作最终将是某种__setitems__操作)

咱们用一个稍简单的例子阐明这一点:

对于以下 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()

其相当于:

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;
}

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

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

4 后记
本文探讨了 Python 变量与指针、援用两大概念之间的关系,次要论证了“Python 不存在援用”以及“Python 变量的行为相似于某种完好的指针”两个论点。

以上就是本次分享的所有内容,想要理解更多 python 常识欢送返回公众号:Python 编程学习圈,发送“J”即可收费获取,每日干货分享

退出移动版