《C++ Primer》关于 reference 和 pointer 部分看的人“生气”

2022-03-15 12:13:14 +08:00
 vcfghtyjc

对 C++一直是一知半解。最近正式的重读《 C++ Primer 》,不知道为什么看到 reference 和 pointer (章节:2.3 )莫名的生气。

不理解当初设计 C++的人多喜欢“&” 和 “*” 这两个符号。为什么不同功能要用同样的符号啊?!"&" 放在定义处代表引用,放到定义过的变量前代表取地址。键盘上那么多符号,就不能换个符号吗?当初大学 C++学的不好这个设计要背一半的锅。😂

7333 次点击
所在节点    C++
64 条回复
xfriday
2022-03-15 23:12:13 +08:00
“ C++ 根本解决不了 C 语言的问题,它只会让事情变得更糟。这真是一门很烂的语言。” -- Linus
FrankHB
2022-03-15 23:31:49 +08:00
@statumer 你这算啥,好歹算是理论上扯得通的。
看着 ISO/IEC 14882 扯什么 polymorphic class 就管 inclusion polymorphism 而对 parametric polymorphism (template) 和 ad-hoc polymorphism (coercion, overloading) 睁眼瞎就来气。
今天还刚婊过 structural bindings 不算 variables (放置 IEC 2382 的定义)就是设计者菜(搞得一开始都没法 capture ),除了能偷懒不改 decltype 的规则以外一点“好处”都没有。
当然这些比起 golang spec 这种指 object 叫 variable 的玩意儿来讲,血压上升效果还不那么明显。
FrankHB
2022-03-15 23:34:31 +08:00
FrankHB
2022-03-16 00:49:59 +08:00
@vcfghtyjc 算了正经回一下 OP 吧。
引用虽然没什么大不了的,但是左值右值这种 1960 年代的经典基础常识理解不了,建议回炉,否则以后基本只能混所谓 GC 语言的饭了。
cf. https://en.wikipedia.org/wiki/Fundamental_Concepts_in_Programming_Languages
很少有单独语言特性的重要性超过这种基本设计。一个少数例外是 1930 年代的 lambda 。

一开始叫左右是因为 CPL 里的原始设计就是按文法性质分的。之后 BCPL 和 B 里就把意图解释明白了:
https://www.bell-labs.com/usr/dmr/www/bcpl.pdf
https://www.thinkage.ca/gcos/expl/b/manu/manu.html#Section6_2
C 也一样,C++只是多加了几种。
当然这个设计就结果上其实也不咋地,当年还有合并引用和左值的: http://www.open-std.org/jtc1/sc22/wg21/docs/papers/1992/N0130.pdf ;很大原因是因为 C 的左值没算到类型系统里。
FrankHB
2022-03-16 00:58:11 +08:00
@yulon 建议回炉重修 C 的声明语法。
C++的&、C++/CLI 的^,都是 C 的*声明符设计的直接扩展,区别无非是可以声明指针的指针而不能声明引用的引用。
而 C 事实上就从来就没什么左边结果类型右边表达式的语法要求。
一个权威实例:<signal.h>的 signal 函数的原型声明在 ISO C 原文中的写法:
void (*signal(int sig, void (*func)(int)))(int);
请自行脑补什么叫左边什么叫右边。
(顺便,所谓“右左法则”也是错的,实际的 parser 不照这个方式工作。)

@dongfang 不用解释这么多,言多必失。
对年轻人讲清楚最简单重要的一点即可:指针是对象类型的一类,引用是和对象类型的并列的一大类类型。
(我终于想起来这种经问题我怎么会没总结过……)
https://stackoverflow.com/a/54731129
你的说法的一些技术问题:
1.&作为声明符会和操作符混淆,首先是因为上下文到底是声明还是表达式就有可能混淆——你不能把不存在混淆的假设作前提。这种问题没法用括号消除(括号自己同时能构成声明符和操作符而具有歧义),像 C 的*( C 的语法光是声明和表达式部分就不是 CFG )。而 C++因为标点在各种上下文的滥用,远远严重得多。语言明确了消歧义规则也不表示没问题,例如很多时候不经过实例化根本就不能区分到底是不是 well-formed ,导致语法分析依赖语义的判断(实际的实现套路是把各种路径都试一遍看看是否恰好有一种是通的)。这个意义上来讲,C++基本上就没有传统意义上的语法(syntax)。当然,这个问题和 OP 的抱怨没有关系。
2.别名是过于笼统的说法,正经的“别名”(按提案和标准里的规则)是专属于 structural binding 的(虽然我有提过这个设计很烂)。而引用声明之后引入的是正儿八斤的变量,并不只是别名。
3.指针和引用首先是类型而不是类型的居留(如表达式的值)。
4.指针的值不保证是所谓“内存”中的“地址”。(先无视 memory 是不是该翻译成内存的问题。)函数指针就不保证值对应所谓内存中的地址;事实上标准中虽然提了函数地址,却根本就没扯清楚含义(跟对象指针有 memory model 和 object model 罩着不一样),至今云里雾里(但是因为先前 CWG 的低效我暂且没有心情去找茬)。
5.基本上所有的 C++编译器都不会用指针去实现引用,因为基本上在前端就已经把类型信息都消化完了。两者可能翻译成同样的 IR 并且可能二进制兼容,但翻译的路径是并行的,并非一个代替另一个。生成的代码里有 ISA 汇编意义上的指针,但和前端的指针完全两码事。
6.重写为#define a b 违反了比几乎所有语义规则原则性上都重要得多的 translation phase 的要求,基本上除非你把预处理器实现成一个完整的解释器都无法使用这种方式实现。

另外几个认为引用是指针的对号入座重修吧,不回了。
vcfghtyjc
2022-03-16 02:53:26 +08:00
@FrankHB 左值 /右值能详细讲讲吗?或者有比较好的资料可以分享吗? wiki 上看的一知半解。
wffnone
2022-03-16 05:56:59 +08:00
@vcfghtyjc 我建议是,你问问题最好讲清楚你到底哪里不懂。我知道,我知道,你不知道你哪里不懂,但是你也要尽量去说一说。。好心给你们讲知识的网友是见一次少一次,,,,且问且珍惜啊。
yulon
2022-03-16 06:21:18 +08:00
@FrankHB 不是很懂你解释了半天在解释什么东西?

void 是结果类型,(*signal(int sig, void (*func)(int)))(int) 是表达式,我也说过函数指针嵌套就是一个经典例子啊?

就像 int *a, b; 你给我说说看,这个 * 是属于左边的 int ,还是右边的 a 啊?你不会和我说 b 也是 int 指针吧?
vcfghtyjc
2022-03-16 07:28:02 +08:00
@wffnone 哈哈,我没有看不懂,我就是吐槽一下。😛
darknoll
2022-03-16 07:47:28 +08:00
用引用避免了 null 的情况,引用其实就是 type* const
zxCoder
2022-03-16 08:22:28 +08:00
兼容 C 加设计考虑不全

至于楼上 c++大佬们解释的那些都是强行解释。。。

c++这个语言就是这样,很强大,但是乱,难,懂的人少,而且这些人也以乱,难为傲
wffnone
2022-03-16 09:24:42 +08:00
@yulon 我是路过的让我说两句
结果表达式是你的表达啦,iso 里面的确没有这个说法。当然你这样理解也有你的道理。
咱也不是新手程序员了吧,就别争了。我觉得 FrankHB 讲的非常细节,普通写应用程序可能用不到,但是在写库或者做一些对代码的自动化处理的时候,都是需要考虑的点。。
aneostart173
2022-03-16 10:11:35 +08:00
个人除了 const 引用之外,一律用指针。。
Cloutain
2022-03-16 11:03:11 +08:00
C++的语法有些地方确实糟糕。。。但不用生气,换种语法舒服的就行了,比如 C#
yulon
2022-03-16 12:49:12 +08:00
@wffnone 一门语言的初期设计本来就是混乱的,所以后期才要标准来硬性规定,C 初期的奇怪语法也删了很多。

计算机编程本就是运用逻辑的过程,你不尝试去理解其中逻辑,只想着死记硬背,这固然是没有问题的,个人选择罢了。

但是拿标准当令箭,动不动就让别人回炉重修,我好像也没说我说的是标准,你怎么知道我没看过标准,这两件事冲突吗,这两件事完全不冲突啊,难道在第一份标准出台以前,这个语言是不存在设计与逻辑与应用的?

Dennis Ritchie 和 Bjarne Stroustrup 都明确表达过标准是用来约束实现 /供应厂商的( 2000 年 7 月的采访)。

比如你花钱买了一份编译器,这个编译器却故意不实现某个功能,这样用户就受到了的损害,其实就是类似合约的东西。

而不是让一些用户把这种精炼过的规则当圣经来制裁他人用的。

我只能说做卷子可能确实是一种 gift 吧。
FrankHB
2022-03-16 12:55:37 +08:00
@yulon 指出你的问题就没花什么时间过脑子。显示的是自以为是的理解荼毒的状况。
(*signal(int sig, void (*func)(int)))(int) 是个声明符,压根就不是什么表达式。
我建议你把教会你这样混淆表达式和声明符语法的理解的文献都列出来,方便给人排雷。

int *a, b; 里的 *a 和 b 都是声明符,*当然是属于(appertain)a 的。但挨在声明符边上的语法元素是否同时影响整个声明又是另一回事,要推断出对声明了什么的影响还早得很,更别说从属还得消歧义的了(比如属性)。

排开混淆问题,这例子正好你是继续在给自己挖坑。
void (*f1(int sig, void (*func)(int)))(int), *(*f2(int sig, void (*func)(int)))(int);
——什么叫左边什么叫右边?右边是个什么?
FrankHB
2022-03-16 13:27:03 +08:00
@yulon C 的初期设计可未必有 C++现在这么混乱,然而规定了也没卵用。
“奇怪语法删了很多”你这是在打 C 的脸么,K&R C 的无原型函数声明一开始就被标记过时结果 C23 才被提议移除: http://www.open-std.org/JTC1/SC22/WG14/www/docs/n2841.htm

“不尝试去理解其中逻辑,只想着死记硬背”亏你说得出来。你所谓的左边右边根本就不是死记硬背,而是生造原本不在语言设计中的逻辑。
这反而才是你所谓的“精炼过的规则”。因为这种逻辑撑死只是覆盖了语言规则子集的二道贩子理解,并没能力涵盖原本的规则。
然而正经的实现又不可能迁就这种半吊子理解。按你“固然是没有问题的”说法,结果就是得两套共存。
(也不是说这算是你的独创。类似的二道贩子到处都是,还有各种二次创作的演绎,比这个更过分的公然颠倒黑白篡改原始设计都有,比如“C 没有对象”。)
这种状况市场上是验证过的,典型例子是谭浩强——这类二次创作讲啥啥不清楚,就是非常擅长和你类似的自以为是的发明,然而自己又 fork 不了语言规范自己开发实现当作一个新的方言,所以注定就是不入流。
有用户看到这种“个人选择”就往里跳,结果就是浪费时间凭空制造碎片。为了纠正二道贩子理解还得回炉,给正常教学和用户社区添乱。
所以吃饱了撑着推销这种错误理解?
选择半吊子发明的规则代替健全的规则,且不说理解多少内容——会支持这种思路的,本来就不算“学会”;这自然应当回炉,毫无疑问。

注意 DMR 和 BS 所谓的标准约束厂商说的是标准化过程中的问题,不表示其他用户就应该有更优先的来源覆盖标准作为语言规范的效力。
以为只有厂商才参考标准,正是这种学习思路下明确的误解。事实是不管什么用户想要报关于语言而不是方言的 bug ,首先参考的就是语言规范这种对任何想要符合规范的实现而言的规格说明书。
忽略规范的后果就是用方言偷换主题。你说 C 人家会有兴趣跟你讨论,你说我就要谭浩强 C ,除了有兴趣研究历史和挂婊数落的,几个人鸟你?
挂羊头卖狗肉还理直气壮,这就是道德问题了。
yulon
2022-03-16 13:37:32 +08:00
@FrankHB 啥叫继续挖坑,这不是很明显的事吗,左边依旧是 void ,右边是 (*f1(int sig, void (*func)(int)))(int) 和 *(*f2(int sig, void (*func)(int)))(int) 啊,不管你再写得怎么花里胡哨都一样。

为什么符号是声明符的一部分,而不是类型的一部分,为什么类型只用在左边写一次,这个部分标准有说明吗?

说白了咱俩讨论的根本就不是一个层次的东西,我本来就在讲标准以外的东西,我之所以讲标准以外的东西,因为标准所有人讲都一样,那么自己去看就好了,而你误以为我没看过标准,然后不用脑子举一些根本不能反驳我的例子,就非常的奇怪。

举个例子:
小孩:为什么国际象棋一方只有一个王。
大人 A:因为大部分国家只有一个王。
大人 B:建议 A 回炉重修规则,规则里一方只有一个王。
FrankHB
2022-03-16 14:17:47 +08:00
@yulon 所以你所谓的右边是个啥?“右边表达式”?
得了吧。
你到现在还不承认你混淆表达式和声明符的入门级错误。

我反对左边右边的笼统分法(而不只是左边具体是什么右边又是什么),首先是因为这东西也就是一种文法的实例(都算不上特例)——文法上极少有递归到自己的分类,可以说到处都构成文法的元素是“××”在“××”的左边或者右边(或者还有递归的),比如表达式中某个操作符和操作数之间的顺序关系。(考虑到操作符数量不少还有更多子类,划分优先级结合性之类辅助记忆倒是情有可原,起码没直球“左”“右”上来就那么白开水废话。)
声明的文法何德何能体现需要死记硬背的独特性?鼓吹这种死记硬背只会挖更多坑。
如果你非要说左边和右边,左边是声明指示符,而右边是可能带有初值符的声明符列表。声明符列表进一步 parse 才 reduce 掉逗号。
这种东西在标准里声明的形式文法里非常清楚,被你这么一描述反而引起混乱。
你所谓的左边也有问题。声明指示符就不只是表示类型,也可以是其它的。像 inline 这样的函数指示符,也是左边的一部分。
所以 [左边结果类型,右边表达式] 明显就没一个是对的。

关于声明的文法规则,我懒得抄,但是回答一些基本的问题,至少比你自己造个漏洞百出的来说,毫不费劲。
“为什么符号是声明符的一部分?”(我姑且理解所谓的符号是指 * )——文法规定。
“不是类型的一部分”(我理解为类型指示符)——文法规定。
“为什么类型只用在左边写一次”(我理解为“左边”的声明指示符)——文法规定。

“因为标准所有人讲都一样,那么自己去看就好了”——不同版本的形式文法其实可能有修改,所以这里我都没对形式文法原文引用(讲清楚你的问题也还用不到那么具体)。
当然,跨语言的差异很可能更大。构成声明符的 * 在 C 的文法里叫 pointer ;这部分在 C++ 对应的叫 ptr-operator ,这个叫法引起歧义和违反 PL 传统,就比较山寨。正好,理解了 ptr-operator 里可以有 * 也可以有 &,OP 的直接问题就算是基本解决了。而你在这里和稀泥公然对着干,就很莫名其妙。

另一方面,硬说声明文法的独特性,那会涉及具体是什么的问题——这种划分声明指示符和声明符的设计的反人类味儿太冲了。而且确实也就 C 和 C++ 这样的过气古董设计里使用。
跟 B 语言引入 ++ -- 主要就是为了省代码长度(一开始我猜的,因为想不出技术好处;后来看 K. Thompson 的理由确实如此)一样,中缀声明符语法设计也算是一种复用左侧元素的“压缩”技术,实践上没什么别的技术优势。
这种设计非常不便于非形式地简单描述规则,反而逼得二道贩子出世。你这种错漏描述也是拜其所赐。
wffnone
2022-03-16 17:40:01 +08:00
啊好看。

我就不 at 你们了,我觉得你们的视角不一样。一个是工程师的视角,一个是技术专家的视角。对于新手来说,根据自己的偏好,学习的方向会有取舍。这些知识有什么用?对有些人,知道太多会降低生产效率;对有些人,他们就使用这些知识创造价值。

我觉得来论坛的网友大部分是初学者。什么是好的心态呢?一方面,不要认为任何技术难点是难以理解的,你如果觉得对某个概念的模糊理解妨碍了编程,总是可以在任意时候花一点时间去学透任意概念。如何去学透呢,当然是看第一手的资料和资深技术专家给出的第二手资料。另一方面,也不用刻意追求把一切概念都搞清楚:你没有那么多时间,你会慢慢找到自己的方向。

这个时代的垃圾太多了。一些无良商人和无知的初级程序员写的教程,遍布了整个互联网。他们不去学着怎么把文章写得容易懂,怎么把概念描述清晰,只是挖空心思想着怎么吸引人。作为技术人,我们都讨厌这些忽悠,这些可恶的骗子,这些社会的寄生虫。啊这是我们的共同点吧。

这是一个专为移动设备优化的页面(即为了让你能够在 Google 搜索结果里秒开这个页面),如果你希望参与 V2EX 社区的讨论,你可以继续到 V2EX 上打开本讨论主题的完整版本。

https://www.v2ex.com/t/840451

V2EX 是创意工作者们的社区,是一个分享自己正在做的有趣事物、交流想法,可以遇见新朋友甚至新机会的地方。

V2EX is a community of developers, designers and creative people.

© 2021 V2EX