1 什么是指针
本文所谓的指针(pointer) ,是指C和C++等语言中的内建的语言特性。
在不同范畴中指针这个概念有所不同。在体系结构规范中,指针指称特定的整数字节地址或者两个地址的差(地址偏移量),是整数数值;而在C和C++中,作为核心语言特性支持的指针是一类类型的统称。这两种完全不同的概念经常被混淆,造成一些稀里糊涂的问题(和数组混在一起的时候尤甚)。除非另行说明,本文总是指后者,并不对此进一步展开论述。
C和C++中,指针(右)值是具有指针类型的(右)值。指针值有时也会被和指针混淆,但在健全的理解下通常能消歧义,因此问题不大(数组也有类似的情况,但涉及转换,问题相对严重)。为清晰起见,在这里不会不加区分地使用。
注意,C++的成员指针(pointer-to-member)明确地不是指针。尽管它的数值表示在一些情况下可能被实现为地址的偏移量,但语言中并不存在这种保证,实际也通常不那么简单。重复:成员指针不是这里讨论的指针。
此外,C++中,除了作为语言特性支持的内建(builtin) 指针,也有所谓的智能指针(smart pointer) 。后者在概念上也被 ISO C++11 以来的标准库正式支持。这里讨论的指针不包括这些智能指针,尽管后者和主题相关,并且会在下文重点讨论。

2 什么是设计
这里讨论的设计,是指语言的设计,也就是语言规则的作者决定语言特性中应该存在什么和不存在什么的决策之下的抽象结果。
用户如何使用指针即语用问题是和本文主题相关的问题,会一并讨论,但和这里的设计是两个不同的话题。

3 什么是糟糕
糟糕是一个形容词。
形容设计的糟糕从两个递进的视点得出:对照设计要解决的问题,即需求;对照同类解决方案,即语言中的其它特性或应用领域有交集的其它程序设计语言中的特性。
通俗地,糟糕以“不好用”和“并非不得不好用”来表现。
注意因为语言规则之间的相互作用,是否“好用”或者说要解决的问题,须结合使用场景下的其它问题一并讨论:一项特性即便能很好地解决某些问题,但若几乎总是引起其它难以回避的问题,那么至少是“不够好用”的;而要造成的问题麻烦到一定程度时就显然“不好用”了。

4 指针有什么用
在说明不好用之前,首先需要了解有什么用。
这是一个发散的语用问题,但大多数用法都很浅显,清楚语言规则就并不难归纳。

4.1 指针和地址
C/C++的指针值和体系结构中的所说的指针(地址或地址偏移量)的基本作用类似,它用来指示数据存储的位置。
以体系结构的接口实现C/C++,可以轻易保证相同类型的指针值到地址的映射是单射,即相同指针类型的指针值的不同的数值表示可以总是找到不同的地址对应,这样就可以在整数算术和关系操作的基础上毫无额外代价地定义指针算术和关系操作;而指针上的操作符*抽象的正是间接寻址操作。这就是一些用户口中的所谓“接近底层”。这种简单直接实现的最大好处就是容易以非常小的代价生成针对特定体系结构的代码。
一个需要注意的关键不同点在于,C/C++作为强类型语言(这里的用法也比较乱,指的是原本意义——默认具有静态类型检查),其中的值(value) 脱离类型讨论并没有意义,指针值也不例外。对象指针可以进行算术操作,但和整数地址算术的含义并不相同,这受到具体指针类型的影响——例如,T*和整数的+操作和sizeof(T)相关;而函数指针并没有类似的意义。此外,需要不同间接操作层次的值如T*和T**也可被明确地静态地区分,光靠地址并不能做到这点。
然而,因为体系结构(硬件)实现的惯例,这个差异在往往能被利用(典型地,基址变址寻址),生成相对高效的代码。这是语言中保留指针算术的用途之一。另一方面,把地址相关的整数数值明确和一般的整数值区分,也明确的目的,使静态检查非预期的混用成为可能,有限地提供了类型安全性(例如,指针和指针不能相加)。
通过两个地址,或一个地址和表示字节大小的一个非负整数就可以标识出地址空间的区间范围。把地址替换为对象指针、字节大小替换为长度(指针值指向的连续元素的个数)同时限制取得指针的手段,能保留这种标记连续存储区域的功效,同时提供一定的可移植性。这种连续的存储在类型系统上被抽象为数组。不过应当注意,在可移植的要求下,实际上指针的语义依赖于数组。完全绕过数组的存在任意地构造一个指针值不能保证指向有效的对象或函数,进行间接操作基本上总会导致未定义行为。
另一个关键不同是空指针值(null pointer value) 并不保证有特定的地址对应,见下文。

4.2 存储资源管理
因为一个对象指针和长度可以用于表示连续的内存,而对象(排除VLA)的大小能在翻译时静态确定,所以在已知大小的存储区域可以用一个对象指针值直接表示。
ISO C标准库的malloc和calloc以及ISO C++标准库的::operator new和::operator new[]等的返回值是典型的实例。
这里大小是由存储分配另外保存,这样释放时仍然只需要传递一个指针值即可。ISO C++14提供了sized deallocation,不过这并非强制。

4.3 参数传递
因为从分配函数中取得的指针表示的存储并不会如自动变量一样会被自动回收,同时指针有间接操作,而指针值作为对象类型的值可以作为参数传递,因此传递指向对象的指针值配合间接操作就可以模拟传递对象的引用。

4.4 基于存储的迭代
因为对象指针能表示存储位置,连续存储的布局由存储模型(以及基于数组的语义)规则限定,适当类型的指针值进行算术操作可以双向顺序迭代乃至随机访问连续存储的对象。

4.5 空指针值
指针类型是可空类型(nullable) 类型,约定特殊的空指针值表示不指向任何对象或函数,但可以进行有限的比较。
可空类型很容易用来表示可选(optional) 值:约定空指针表示值不存在,非空指针指向的对象或函数即存在的可选值。
空指针值还可以表示哨位(sentinal) 即迭代终止的标识。相对于具体存储区间结束的指针相比,空指针值是通用的,并不需要根据特定的区间使用不同的值。
注意空指针值的存储表示不一定是整数零值(这再次体现了人为预选的数值和地址的无关性),尽管使用零值一般能有更好的初始化性能。

5 指针为什么不好用
既然标题已经确定了指针设计的糟糕,那么在“不好用”上自然有充分的理由。
总结就是,指针看上去能干很多事,但没一样事是完全干好的,还有的事(比如声明语法)甚至在一般意义上就特别差。
讽刺的是,第一个大规模使用这种指针的C语言作为UNIX系统的实现却完全违反了UNIX程序鼓吹的模块化设计哲学:只做一件事,并且把事做好。
为什么“程序”应当遵守的原则,分解到实现语言的特性的层次上,就可以罔顾设计原则乃至表现得相反了呢?难道这里不更应该体现接口的可组合性吗?耐人寻味。

5.1 使用的意图
首要的原罪就是指针能干太多事了,导致如果只需要其中的某些功能子集(几乎所有情况都是这样,实际上也不可能全用上,见下文)就不容易看清楚代码在做什么,也就是任何“正常”的使用与使用其它替代实现手段相比,都很容易明显损害代码的可读性。
要避免这点,要么放弃使用指针而使用其它替代,要么就不得不以文档(包括注释)等形式来把这些接口规格中大多不必出现的琐碎细节约定清楚。后者很容易显著增大实现和维护的工作量。

5.2 易错
因为意图不明的关系,使用指针的代码比使用其它更清晰的替代的代码更有机会错误,而指针本身的静态类型检查对此爱莫能助。
最显著和严重的错误可能是对于存储资源管理的错误。
注意C/C++语言要求去配函数的指针值参数若非空,则必须和适当的分配函数的返回(指针)值相等且不能以相同的值调用去配函数超过一次,否则程序行为未定义。
因为指针值并不保证翻译时确定,静态检查对此类误用效果很有限,要想安全使用且不泄露资源,用户必须清楚使用的指针是否可以被释放,然后准确保证从分配函数得到的非空指针值恰好作为参数调用正确的对应的去配函数一次——这里是否可以释放的所有权(ownership) 信息并没有编码在指针的类型之中。
注意,单看一个指针值,有或者没有所有权是确定的,不存在第三种状况。鉴于这两种状况互斥,因此一个指针值不可能同时是表示存在所有权的资源指针和表示不存在所有权的资源视图/观察者指针。这也就是上文说“不可能全用上”的原因。
然而事实是明确持有一个有所有权的指针,需要释放时,光看指针根本没法知道该使用哪个去配函数……更有甚者,其实光从指针上根本就看不出有没有所有权。
如果一个返回指针值的函数不幸没有文档描述清楚用户该如何处理资源释放问题,就面临了两难的风险:调用错误的去配函数或重复释放导致未定义行为,或者放置不管而至少产生泄漏的后果。
可能就是因为这样,WG14( C 标准委员会)有一条不成文的规矩:返回指针的函数总是不带所有权——也就是用户不应该释放这里的资源。
然而就连 Austin Group (起草 POSIX 标准的作者)对此都并不买账(更别提 GNU 等了),造成了接口设计上的冲突(详见 WG14/N1174 ),可见这条默许的规则在 C 用户的范畴内整体上行不通。
用户该何去何从?看脸……(没有接口文档的自己踩坑怎么死?看着办。)

5.3 不必要的负担
这里最明显的例子是明明静态确定在不需要空指针的情况下不得不判断指针值是否为空,给程序运行带来不必要的开销。
所谓的“空指针”滥觞于C.A.R.Hoare在1960年代的ALGOL W语言的发明。2009年,Hoare在一个会议上为此道歉,原因是空指针特性引发了很多程序设计中的错误和漏洞。
盲目省略空指针值检查的导致使用指针间接操作的值引起未定行为的错误威力并不比上面资源管理的错误来得小。因此一旦接口沾染了指针,事情就复杂了——最容易的修复就是放弃使用指针这样的可空类型。

5.4 语法噪音
上面说的都是语义直接相关的语用困难。
事实上,即便不考虑语义问题,经验表明光是 C/C++ 的指针语法(严格来说不光是指针自己的问题,还有数组、 C++ 的引用和 C++/CLI 的句柄等,都属于此类)也相当反直觉了。大部分用户遇到嵌套的指针声明符甚至都不能一下子看明白边界,更别说表示什么意思了。
对这个问题的主要变通是使用 typedef 。但未必每个接口都会老实用——比如 ISO C 的 signal 函数就没有用。所以遇到了用户还是要硬着头皮看。另外还可能有同时有使用 typedef 和不使用 typedef 名称并存然而两者等价的局面,此时用户就得当人肉编译器自行验证 typedef 和复杂声明符的等价性了……
而现代的编译器也没能利用这样的语法带来简化。
鉴于这种看起来精巧实则无用的设计带来的困难,Bjarne Stroustrup 等在 C++ 尝试引入更直白的语法。但是,虽然 trailing-return-type syntax 是引入到 ISO C++11 里了,兼容 C 却不能排除旧的语法,结果就是对用户来说存在两套不完全兼容语法要学,编译器也得把两套语法都实现这样一个混乱局面……

5.5 语义噪音
同样因为意图不明的关系,要让不同用法之间存在差异变得困难了。
举例来说,C++不需要内建指针模拟对象引用传递参数,所以看到->和一元操作*(重载另说,但不抽风的重载不应该和这里的清晰性背道而驰)就可以大致确定此处进行的是非平凡(模拟参数传递)的操作。
考虑到模拟引用参数也必然不需要空指针值,这样一来差距更明显。

5.6 抽象的无能
或许抽象能力的缺失才是最大的现实问题,因为关乎高级语言的本质目的,而并非特定的个别需求。
一个例子是,迭代存储连续的序列用算术操作,为什么同样是迭代,链表就不能类似的语法呢?
不过只是“不好用”的角度并不容易集中体现这一点,此处先略过。

5.7 互操作性
和体系结构的交互或许是指针唯一合适的领域了。不过,这依赖于实现的假设,因此操作起来并不那么有普适性。
即便平时鼓吹“硬件友好”“接近底层”,事实上 C 就不存在对地址空间的抽象,还得靠厂商或者 WG14/N1169 这类几乎名不见经传的扩展。
倒是 C++ 标准库的分配器机制本来有要支持上面扩展的考虑不同的指针,虽然后来都流行平坦地址空间然后这个需求就死得差不多了……
一个根本硬伤是,相同类型指针值到地址的映射是单射而不是满射——也就是任意一个地址即便在体系结构和实际机器的环境下允许,也有重重限制,根本不保证能用能自由操作的指针表示。
这样,关键时刻到底还得上体系结构相关的扩展乃至汇编和/或机器语言……(什么硬件友好接近底层,见鬼去吧(╯`□′)╯(┻━┻!)

5.8 理解的混乱
事实证明,指针自身的微妙规则以及和数组之间看似说不清道不明的关系给教材编写者以及初学者带来了极大麻烦。
不管是 Bjarne Stroustrup 鼓吹的 teachability 还是一般用户期待的“易用性”,指针的语法和语义规则都是重灾区。
总体来看,这种的问题的根源来源于指针这项语言特性自身的设计——包括是不是真的适合作为核心语言特性这点。

5.9 谁来承担责任
可笑的是,缺陷这样明显的语言特性,一边在被各种集中地滥用和误用,一边被井底之蛙吹嘘为“ C 的灵魂”骗更多不知情者上当……
容忍这样的缺陷和制造混乱代码的作者通常是同一拨用户。对于不良语用导致的后果却往往由合作的理解更透彻的维护者承担,把本可以满足更多现实需求的时间花在给脑残粉的烂代码擦屁股的破事上。
这是有多不公平呢?

6 “指针”必须这样不好用吗/不用指针用啥
如果不限于内建指针,答案是否定的。
从指针几个有用和常用的使用惯例来看,搞清楚真实需求之后,很容易设计出更安全好用的机制。当然,得有足够的其它核心语言特性支持,类型系统羸弱的 C 只能靠边站。
对这里的缺陷修正得比较彻底而又比较流行的例子主要就是 C++ ,同时 C++ 也保留了指针的操作,反而更有必要澄清什么时候不适合用指针。所以以下以 C++ 为例(涉及的主要特性,其它现代语言,即便没有指针也大多有对应)。

6.1 了解意图、避免常见错误和提升可读性
若需要间接操作表示资源,使用带所有权的智能指针。同时可以自动管理资源,避免资源泄漏。不加封装地使用内建指针意味着更混乱的代码路径,通常是糟糕的代码。
若需要间接操作表示不带所有权的资源视图,使用不带所有权的特定指针类型,如 WG21/N4282 提议的 observer_ptr 来帮助表明意图。(这里使用内建指针的问题相对比较小,在没有其它选择的偷懒情况下,使用内建指针相对来说能够被容忍,因为带有所有权的指针已经被其它智能指针区分出去了。)
若需要传递引用,直接使用内建引用。在需要复制引用的场合,使用 std::ref 之类的包装。内建指针在此本质上毫无必要,并且无法使用大部分其它设施(只有 std::bind 等一些少数例外)。
若需要可空类型,使用 WG21/N4480 等规范的 optional 类型。(内建指针仍然是个可以忍耐的替代,但并不推荐。)
若需要迭代操作,使用迭代器(iterator) 。迭代器同时有更好的类型安全性、适应性和可扩展性。指针作为随机访问迭代器的特例是可以被使用的,但仍然应当小心行事。
通过划分典型应用场景,就基本解决了上面的最麻烦的一些问题。除了静态区分存在和不存在所有权相互矛盾之外,以上类型也是可以组合的,因此同时需要多种意图也不需要使用内建指针。

6.2 抽象能力和可扩展性
这集中体现在智能指针和迭代器与内建指针的对比之上。
内建指针的语义基本是被核心语言规则写死的,它并不能实现智能指针这样用户自定义资源所有权管理策略,以及迭代器这样的适配于不同实现构造的序列上。因为过于特殊,可以说是相当地无能。高下立判。
通过迭代器类别(iterator category) 的抽象层次和 tag dispatch 这样基于重载(说穿了,一种模式匹配)的技巧,还能实现对不同性质的序列静态自动选取最优算法。不知比指针高了哪里去了。
当然,内建指针和典型体系结构实现之间的能力仍旧没有被取代。但指针在真正底层(比如说,地址空间)的抽象仍然一直是个坑。而且这明显不是高级语言的本职工作。如果不是照顾兼容性,让厂商实现成扩展并用标准库包装,说不定还不会像现在那么混乱。

6.3 约束更强的设计
(现代) C++ 是强烈强调静态类型存在感的语言。这种设计有利有弊,但从实践效果来看,正确地使用能够发挥静态类型检查的优势,是当代软件工程实践的重要趋势之一。(静态类型当然有非常鸡肋的地方然而现实是大部分用户根本连边都碰不到……注意缺乏元数据是 C++ 和标准化的锅,不是静态类型的锅。)
但是 C++ 限于历史包袱(兼容 C 、兼容现在各种代码),即便比 ISO C 敢于甩手扔包袱,也得考虑一下现实影响。在这个意义上,用户相对较少的小众语言以及新设计的语言就没有那么多顾虑,能将有目的的设计刻意发挥得更充分。
举两个稍微不怎么小众的例子。
一个是 Haskell 。应该说重点不纯粹是静态类型的问题,而是在类型系统的设计上使用了对静态分析友好的较为系统化的设计。(而并不是像 C++ 那样一小坨一小坨地加特性,而这里最大坨的 Concept 被否了……)
当然这货主要用于开眼和拓展想象力,因为默认求值策略过于标新立异实际上不适合通用的需求,在 DSL 以上的实用还是算了。
另一个是 Rust 。嗯,设计的主要目标是取代 C++ ,应该还算是比较现实(?)的。在这里值得一提的是有不少设计把上面的策略整合到核心语言特性上去了并且有系统的理论支撑,比如 linear typing 是对 C++ 的 std::unique_ptr 强化。
姑且不论大杂烩的实用程度,这在科普上比较有意义。

6.4 复杂性谁来买单
有的用户可能会说,这么复杂,还是用内建指针直接偷懒算了。
对此我只能表示呵呵。你真有自觉到完全写清楚各个层次的接口文档表明语用?——注意,各个层次,包括现在当成实现细节而将来可能被接手的其他维护者当成内部接口使用的任意层次的“接口”。
如果:
(1)因为非自身原因只能用 C 这等无能玩意儿的而且真做得到及说服了其它倒腾这坨代码的(如果有)也同样做到上面所说的自觉,或者——
(2)保证这坨代码不流入公众视野充实反面教材,同时实现者保证必要时时刻忏悔生产垃圾多出来的碳排放
那么当我没说。
否则……思想有多远就给我滚多远。
又不是叫你发明新语言特性自己实现编译器,都敢倒腾“底层”语言了,了解基本需求和解决方案这么点简单的份内之事都做不好还有脸生产垃圾污染环境让人擦屁股来添乱?
还是有谁逼你用这坨容易炸的东西了?(不懂适应现实?那么饿死活该。)
注意,业界从来不缺猪队友,少一头的确照样转(蠢代码照样蠢)。

7 结语
略。

转载于:https://www.cnblogs.com/think4luo/p/8401142.html

为什么指针是个糟糕的语言特性相关推荐

  1. C++应用程序性能优化(三)——C++语言特性性能分析

    C++应用程序性能优化(三)--C++语言特性性能分析 一.C++语言特性性能分析简介 通常大多数开发人员认为,汇编语言和C语言比较适合编写对性能要求非常高的程序,C++语言主要适用于编写复杂度非常高 ...

  2. python 特性和方法同名_Python语言特性的梳理

    对python的语言特性,多线程机制,以及性能局限的梳理 运行环境 由于Python不同版本,尤其是Python2与Pyhton3之间差异明显,所以运行不同项目时往往需要不同版本的运行环境,这种情况下 ...

  3. 《C++覆辙录》——1.9:使用糟糕的语言

    摘要: 本节书摘来自异步社区出版社<C++覆辙录>一书中的第1章,第1.9节,作者: [美]Stephen C. Dewhurst(史蒂芬 C. 杜赫斯特),更多章节内容可以访问云栖社区& ...

  4. Atitit.rust语言特性 attilax 总结

    Atitit.rust语言特性 attilax 总结 1. 创建这个新语言的目的是为了解决一个顽疾:软件的演进速度大大低于硬件的演进,软件在语言级别上无法真正利用多核计算带来的性能提升.1 2. 不会 ...

  5. 《C++应用程序性能优化::第二章C++语言特性的性能分析》学习和理解

    <C++应用程序性能优化::第二章C++语言特性的性能分析>学习和理解 说明:<C++应用程序性能优化> 作者:冯宏华等 2007年版.最近出了新版,看了目录,在前面增加了一章 ...

  6. java语言概述、java语言特性、java语言发展史、java语言作用

    Java介绍: Java语言概述: Java语言是由美国Sun(Stanford University Network)斯坦福网络公司的java语言之父–詹姆斯·高斯林,在1995年推出的高级的编程语 ...

  7. [转]Objective-C 语言特性

    一. Object-C 的前世今生 Object-C语言由 Brad J.Cox于20世纪80年代早期设计,以SmallTalk为基础,建立在C语言之 上.1988年,NeXT获得Object-C的授 ...

  8. c 11 主要的新语言特性,C 11系列

    什么是C++0x? C++0x是C++最新标准标准化过程中的曾用名,在这一系列文章中我们将介绍最新标准添加的一系列新的语言特性.在2011年9月份,C++0x正式由官方发布并命名C++11,现在很多编 ...

  9. Visual C++ 2005的现代语言特性

    Visual C++ 2005的现代语言特性 本文讨论: .NET C++/CLI语法 配置向导优化(PGO) MSIL优化 OpenMP支持 增强的缓冲区安全检查 当年Visual Studio® ...

最新文章

  1. c#a服务器上传文件b服务器,C#_c#批量上传图片到服务器示例分享,客户端代码: 复制代码 代码 - phpStudy...
  2. Linux驱动如何在不同版本上快速迭代升级
  3. 离开载具_绝地最强载具登场?
  4. ajax get 缓存 ie,Ajax异步同步请求被IE缓存的问题解决方法(get方式)
  5. 利用linux的mtrace命令定位内存泄露(Memory Leak)
  6. VM虚拟机打开其中一个vmx文件没反应问题解决
  7. C++ STL priority_queue的正确使用方法
  8. Android ADB命令?这一次我再也不死记了!【简单说】
  9. Intellij idea 2018.3热部署 jrebel 激活
  10. java毕业设计开题报告javaweb户籍管理系统|户口
  11. (转)CSS实现一个会旋转的太极图案
  12. 文字超出部分用省略号表示
  13. android 全局悬浮按钮,Android自定义APP全局悬浮按钮
  14. 笔记本ubuntu安装xen之殇
  15. 天津理工大学计算机项目管理实验四,天津理工大学软件工程实验报告4.docx
  16. 【程设作业】魔兽世界三:开战
  17. 前端模型--css动画(旋转八音盒)
  18. NLP:MRC常用数据集
  19. 遗传算法(GA)学习 || 原理、本质、代码、例题
  20. 输入学生学号、成绩,并排序

热门文章

  1. 易基因|综述:单细胞DNA甲基化分析方法全介绍及未来发展前景预测
  2. 万能码的码上付全新的体验(安全扫码专业委员会)
  3. 机械臂——六轴机械臂逆解
  4. word中文正常,但是英文空格间距特别大
  5. asuswrt 单臂路由_不用设VLAN,也能搞定单臂路由器
  6. matlab psk 信号,急求···psk信号载频估计的matlab算法
  7. windows和linux常用软件的对应
  8. 如何打开注册表编辑器?
  9. 基于JAVA酒店订房系统计算机毕业设计源码+系统+mysql数据库+lw文档+部署
  10. 蓝桥杯练习 算法提高 阮小二买彩票 无代码