每日一博 | 对比 C++ 和 Python,谈谈指针与引用
花下猫语:本文是学习群内 樱雨楼 小姐姐的投稿。之前已发布过她的一篇作品《当谈论迭代器时,我谈些什么?》,大受好评。本文依然是对比 C++ 与 Python,来探讨编程语言中极其重要的概念。祝大家读有所获,学有所成!
樱雨楼 | 原创作者
豌豆花下猫 | 编辑润色
本文原创并首发于公众号【Python猫】,未经授权,请勿转载。
原文地址:https://mp.weixin.qq.com/s/k0VNL6uyvBHT7PWdzEPROQ
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__操作将进行隐式解指针,通过此方法对变量进行修改等同于通过解指针操作修改变量原值。
此种性质意味着:
- 任何不涉及__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 被绑定到了一个新指针上。
- 任何涉及__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进阶系列、好书推荐系列、技术写作、优质英文推荐与翻译等等,欢迎关注哦。
低调大师中文资讯倾力打造互联网数据资讯、行业资源、电子商务、移动互联网、网络营销平台。
持续更新报道IT业界、互联网、市场资讯、驱动更新,是最及时权威的产业资讯及硬件资讯报道平台。
转载内容版权归作者及来源网站所有,本站原创内容转载请注明来源。
- 上一篇
历经十年,Windows 7 生命周期将结束,微软:快换 Windows 10
距离 Microsoft终止对 Windows 7 的支持只剩半年,到 2020 年 1 月 14 日,这款经典的操作系统就要和大家 say goodbye 了。 2009 年 7 月 4 日,Windows 7 正式开发完成,到现在已经整整 10 岁。它在 Vista 前辈的基础上发展而来,界面友好,功能强大,使用高效,因此成了继 Windows XP 之后的又一棵“常青树”,也成了 Windows 10 普及之路上最大的“绊脚石”。 其实在 2015 年 1 月 13 日,Microsoft 就终止了对Windows7 SP1 的主流技术支持,此后仅发放安全补丁和紧急修复补丁,并宣布在 2020 年 1 月 14 日彻底结束拓展支持,不再提供任何补丁。 但即便到了今天,Intel、AMD 新平台都不再支持的情况下,Windows 7 也依然拥有大量拥趸和广阔市场。 NetMarketShare 数据显示,Windows 7 目前仍旧占据多达 35.38% 的桌面系统市场份额,Windows 10 虽然早就成了第一大系统,但也不超过 45.79%,还没有过半。 StatCounter...
- 下一篇
NVIDIA BERT 推理解决方案 Faster Transformer 开源了
NVIDIA BERT推理解决方案Faster Transformer开源了 Faster Transformer是一个基于CUDA和cuBLAS的Transformer Encoder前向计算实现,其优越的性能将助力于多种BERT的应用场景。 2017年12月Google在论文“Attention is All You Need”[1] 中首次提出了Transformer,将其作为一种通用高效的特征抽取器。至今,Transformer已经被多种NLP模型采用,比如BERT[2]以及上月发布重刷其记录的XLNet[3],这些模型在多项NLP任务中都有突出表现。在NLP之外, TTS,ASR等领域也在逐步采用Transformer。可以预见,Transformer这个简洁有效的网络结构会像CNN和RNN一样被广泛采用。虽然Transformer在多种场景下都有优秀的表现,但是在推理部署阶段,其计算性能却受到了巨大的挑战:以BERT为原型的多层Transformer模型,其性能常常难以满足在线业务对于低延迟(保证服务质量)和高吞吐(考虑成本)的要求。以BERT-BASE为例,超过90%的计...
相关文章
文章评论
共有0条评论来说两句吧...
文章二维码
点击排行
-
Docker使用Oracle官方镜像安装(12C,18C,19C)
- Springboot2将连接池hikari替换为druid,体验最强大的数据库连接池
- CentOS8编译安装MySQL8.0.19
- Docker快速安装Oracle11G,搭建oracle11g学习环境
- SpringBoot2配置默认Tomcat设置,开启更多高级功能
- MySQL8.0.19开启GTID主从同步CentOS8
- CentOS7,8上快速安装Gitea,搭建Git服务器
- Jdk安装(Linux,MacOS,Windows),包含三大操作系统的最全安装
- SpringBoot2编写第一个Controller,响应你的http请求并返回结果
推荐阅读
最新文章
- Jdk安装(Linux,MacOS,Windows),包含三大操作系统的最全安装
- SpringBoot2配置默认Tomcat设置,开启更多高级功能
- Springboot2将连接池hikari替换为druid,体验最强大的数据库连接池
- SpringBoot2整合MyBatis,连接MySql数据库做增删改查操作
- Hadoop3单机部署,实现最简伪集群
- Docker使用Oracle官方镜像安装(12C,18C,19C)
- MySQL8.0.19开启GTID主从同步CentOS8
- SpringBoot2编写第一个Controller,响应你的http请求并返回结果
- Windows10,CentOS7,CentOS8安装MongoDB4.0.16
- CentOS7,CentOS8安装Elasticsearch6.8.6