这是我从台湾的http://www.cis.nctu.edu.tw/chinese/doc/research/c++/C++FAQ-Chinese/发现的《C++ Frequently Asked Questions》的繁体翻译,作者是:叶秉哲,也是《C++ Programming Language》3/e繁体版的译者,该文章是非常的好,出于学习用途而将它转贴,本人未取得作者的授权,原文章的版权仍然归属原作者.
C++语言常见问题解
== Part 3/4 ============================
comp.lang.c++ Frequently Asked Questions list (with answers, fortunately).
Copyright (C) 1991-96 Marshall P. Cline, Ph.D.
Posting 3 of 4.
Posting #1 explains copying permissions, (no)warranty, table-of-contents, etc
=============================
■□ 第14节:程序风格指导
=============================
Q81:有任何好的 C++ 程序写作的标准吗?
感谢您阅读这份文件,而不是再发明自己的一套。
但是请不要在 comp.lang.c++ 里问这问题。几乎所有软件工程师,或多或少都把这
种东西看成是「大玩具」。而且,一些想成为 C++ 程序撰写标准的东西,是由那些
不熟悉这语言及方法论的人弄出来的,所以最后它只能成为「过去式」的标准。这种
「摆错位置」的现象,让大家对程序写作标准产生不信任感。
很明显的,在 comp.lang.c++ 问这问题的人,是想使自己更精进,不会因自己的无
知而绊倒,然而一些回答却只是让情况更糟而已。
========================================
Q82:程序撰写标准是必要的吗?有它就够了吗?
程序撰写标准不会让不懂 OO 的人变懂;只有训练及经验才有可能。如果它有用处的
话,那就是抑制住那些琐碎无关紧要的程序片段--当大机构想把零散的程序设计组
织整合起来时,这些片段常常会出现。
但事实上你要的不光是这种标准而已。它们提供的架构让新手少去担心一些自由度,
但是系统化的方法论会比这些好看的标准做得更好。组织机构需要的是一致性的设计
与实行“哲学”,譬如:强型别或弱型别?用指针还是参考接口? stream I/O 还是
stdio? C++ 程序该不该呼叫 C 的?反过来呢? ABC 该怎么用?继承该用为实作的
技巧还是特异化的技巧?该用哪一种测试策略?一一去检查吗?该不该为每个资料成
员都提供一致的 "get" 和 "set" 接口?接口该由外往内还是由内往外设计?错误状
况该用 try/catch/throw 还是传回值来处理?……等等。
我们需要的是详细的“设计”部份的「半标准」。我推荐一个三段式标准:训练﹑谘
询顾问以及链接库。训练乃提供「密集教学」,咨询顾问让 OO 观念深刻化,而非仅
仅是被教过而已,高品质的链接库则是提供「长程的教学」。上述三种培训都有很热
门的市场景况。(【译注】无疑的,这是指美﹑加地区。)接受过上述培训的组织都
有如此的忠告:「买现成的吧,不要自己硬干 (Buy, Don't Build.)。」买链接库,
买训练课程,买开发工具,买咨询顾问。想靠自学来达到成功的工具厂商及应用/系
统厂商,都会发现成功很困难。
【译注】这一段十分具有参考价值。不过有些背景资料得提供给各位参考。别忘了:
作者是美国人,是以该地为背景,且留意一下他所服务的公司是做什么的..
... 唉!国内有这么多的专业顾问公司吗? :-<
少数人会说:程序撰写标准只是「理想」而已,但在上述的组织机构中,它仍有其必
要性。
底下的 FAQs 提供一些基本的指导惯例及风格。
========================================
Q83:我们的组织该以以往 C 的经验来决定程序撰写标准吗?
No!
不论你的 C 经验有多丰富,不论你有多高深的 C 能力,好的 C 程序员并不会让你
直接就成为好的 C++ 程序员。从 C 移到 C++ 并不仅是学习 "++" 的语法语意而已
,一个组织想达到 OOP 的境界,却未将 "OO" 的精神放进 OOP 里的话,只是自欺罢
了;会计的资产负债表会把他们的愚蠢显现出来。
C++ 程序撰写标准应该由 C++ 专家来调整,不妨先在 comp.lang.c++ 里头问问题(
但是不要用 "coding standard" 这种字眼;只要这样子问:「这种技巧有何优缺点
?」)。找个能帮你避开陷阱的高手,上个训练课程,买链接库,看看「好的」程序
库是否合乎你的程序撰写标准。绝对不要光靠自己来制定标准,除非你对它已有某种
程度的掌握。没有标准总比有烂标准好,因为不恰当的「官方说法」会让不够聪明的
平民难以追随。现在 C++ 训练课程及链接库,已有十分兴盛的市场。
再提一件事:当某个东西炙手可热时,招摇撞骗者亦随之而生;务必三思而后行。也
要问一下从某处修过课的人,因为老手不见得也是个好教员。最后,选个懂得指导别
人的从业人员,而不是个对此语言/方法论只有过时知识的全职教师。
【译注】善哉斯言!
========================================
Q84:我该在函数中间或是开头来宣告区域变量?
在第一次用到它的地方附近。
对象在宣告的时候就会被初始化(被建构)。如果在初始化对象的地方没有足够的资
讯,直到函数中间才有的话,你可以在开头处初始个「空值」给它,等以后再「设定
」其值;你也可以在函数中间再初始个正确的东西给它。以执行效率来说,一开始就
让它有正确的值,会比先建立它,搞一搞它,之后再重建它来得好。以像 "String"
这种简单的例子来看,会有 350% 的速度差距。在你的系统上可能会不同;当然整个
系统可能不会降低到 300+%,但是“一定”会有不必要的性能衰退现象。
常见的反驳是:「我们会替对象的每个资料提供 "set" 运作行为,则建构时的额外
耗费就会分散开来。」这比效能负荷更糟,因为你添加了维护的梦靥。替每个资料提
供 "set" 运作行为就等于对资料不设防:你把内部实作技巧都显露出来了。你隐藏
到的只有成员对象的实体“名字”而已,但你用到的 List﹑String 和 float(举例
来说)型态都曝光了。通常维护会比 CPU 执行时间耗费的资源更多。
区域变量应该在靠近它第一次用到之处宣告。很抱歉,这和 C 老手的习惯不同,但
是「新的」不见得就是「不好的」。
========================================
Q85:哪一种原始档命名惯例最好? "foo.C"? "foo.cc"? "foo.cpp"?
如果你已有个惯例,就用它吧。如果没有,看看你的编译器,看它用的是哪一种。典
型的答案是:".C", ".cc", ".cpp", 或 ".cxx"(很自然的,".C" 扩展名是假设该
档案系统会区分出 ".C" ".c" 大小写)。
在 Paradigm Shift 公司,我们在 Makefiles 里用 ".C",即使是在不区分大小写的
档案系统下(在有区分的系统中,我们用一个编译器选项:「假设 .c 档案都是 C++
的程序」;譬如:IBM CSet++ 用 "-Tdp",Zortech C++ 用 "-cpp",Borland C++用
"-P",等等)。
========================================
Q86:哪一种标头档命名惯例最好? "foo.H"? "foo.hh"? "foo.hpp"?
如果你已有个惯例,就用它吧。如果没有,而且你的编辑器不必去区分 C 和 C++ 檔
案的话,只要用 ".h" 就行了,否则就用编辑器所要的,像 ".H"﹑".hh" 或是
".hpp"。
在 Paradigm Shift 公司,我们用 ".h" 做为 C 和 C++ 的源文件(然后,我们就
不再建那些纯粹的 C 标头档案)。
========================================
Q87:C++ 有没有像 lint 那样的指导原则?
Yes,有一些常见的例子是危险的。
但是它们都不尽然是「坏的」,因为有些情况下,再差的例子也得用上去。
* "Fred" 类别的设定运操作数应该传回 "*this",当成是 "Fred&"(以允许成串的设
定指令)。
* 有任何虚拟函数的类别,都该有个虚拟解构子。
* 若一个类别有 {解构子,设定运操作数,拷贝建构子} 其一的话,通常三者也都全
部需要。
* "Fred" 类别的拷贝建构子和设定运操作数,都该将它们的参数加上 "const":分别
是 "Fred::Fred(const Fred&)" 和 "Fred& Fred::operator=(const Fred&)" 。
* 类别的子对象一定要用初始化串行 (initialization lists) 而不要用设定的方
式,因为对使用者自订类别而言,会有很大的效率差距(3x!)。
* 许多设定运操作数都应该先测试:「我们」是不是「他们」;譬如:
Fred& Fred::operator= (const Fred& fred)
{
if (this == &fred) return *this;
//...normal assignment duties...
return *this;
}
有时候没必要测试,但一般说来,这些情况都是:没有必要由使用者提供外显的
设定运操作数的时候(相对于编译器提供的设定运操作数)。
* 在那些同时定义了 "+="﹑"+" 及 "=" 的类别中,"a+=b" 和 "a=a+b" 通常应该
做同样的事;其它类似的内建运操作数亦同(譬如:a+=1 和 ++a; p[i] 和 *(p+i);
等等)。这可使用二元运操作数 "op=" 之型式来强制做到;譬如:
Fred operator+ (const Fred& a, const Fred& b)
{
Fred ans = a;
ans += b;
return ans;
}
这样一来,有「建构性」的二元运算甚至可以不是伙伴。但常用的运操作数有时可
能会更有效率地实作出来(譬如,如果 "Fred" 类别本来就是个 "String",且
"+=" 必须重新配置/拷贝字符串内存的话,一开始就知道它的最后长度,可能会
比较好)。
==============================================
■□ 第15节:Smalltalk 程序者学习 C++ 之钥
==============================================
Q88:为什么 C++ 的 FAQ 有一节讨论 Smalltalk?这是用来攻击 Smalltalk 的吗?
世界上「主要的」两个 OOPLs 是 C++ 与 Smalltalk。由于这个流行的 OOPL 已有第
二大的使用者总数量,许多新的 C++ 程序者是由 Smalltalk 背景跳过来的。这一节
会回答以下问题:
* 这两个语言的差别?
* 从 Smalltalk 跳到 C++ 的程序者,要知道些什么,才能精通 C++?
这一节 *!*不会*!* 回答这些问题:
* 哪一种语言「最好」?
* 为什么 Smalltalk「很烂」?
* 为什么 C++「很烂」?
这可不是对 Smalltalk 恐怖份子挑衅,让他们趁我熟睡时戳我的轮胎(在我很难得
有空休息的这段时间内 。
========================================
Q89:C++ 和 Smalltalk 的差别在哪?
最重要的不同是:
* 静态型别或动态型别?
* 继承只能用于产生子型别上?
* 数值语意还是参考语意 (value vs reference semantics)?
头两个差异会在这一节中解释,第三点则是下一节的讨论主题。
如果你是 Smalltalk 程序者,现在想学 C++,底下三则 FAQs 最好仔细研读。
========================================
Q90:什么是「静态型别」?它和 Smalltalk 有多相似/不像?
静态型别(static typing)是说:编译器会“静态地”(于编译时期)检验各运算
的型态安全性,而不是产生执行时才会去检查的程序代码。例如,在静态型别之下,会
去侦测比对函数自变量的型态签名,不正确的配对会被编译器挑出错误来,而非在执行
时才被挑出。
OO 的程序里,最常见的「型态不符」错误是:欲对某对象激活个成员函数,但该物
件并未准备好要处理该运算动作。譬如,如果 "Fred" 类别有成员函数 "f()" 但没
有 "g()",且 "fred" 是 "Fred" 类别的案例,那么 "fred.f()" 就是合法的,
"fred.g()" 则是非法的。C++(静态地)在编译期捕捉型别错误,Smalltalk 则(动
态地)在执行期捕捉。(技术上,C++ 很像 Pascal--“半”静态型别--因为指
标转型与 union 都能用来破坏型别系统;这提醒了我们:你用指针转型与 union 的
频率,只能像你用 "goto" 那样。)
========================================
Q91:「静态型别」与「动态型别」哪一种比较适合 C++?
若你想最有效率使用 C++,请把她当成静态型别语言来用。
C++ 极富弹性,你可以(藉由指针转型﹑union 或 #define)让她「长得」像
Smalltalk。但是不要这样做。这提醒了我们:少用 #define。
有些场合,指针转型和 union 是必要的,甚至是很好的做法,但须谨慎为之。指针
转型等于是叫编译器完全信赖你。错误的指针转型可能会毁坏堆积﹑在别的对象记忆
体中乱搞﹑呼叫不存在的运作行为﹑造成一般性错误(general failure)。这是很
糟糕的事。如果你避免用与这些相关的东西,你的 C++ 程序会更安全﹑更快,因为
能在编译期就检测的东西,就不必留到执行期再做。
就算你喜欢动态型别,也请避免在 C++ 里使用,或者请考虑换另一个将型态检查延
迟到执行期才做的语言。C++ 将型态检验 100% 都放在编译时期;她没有任何执行期
型态检验的内建机制。如果你把 C++ 当成一个动态型别的 OOPL 来用,你的命运将
操之汝手。
========================================
Q92:怎样分辨某个 C++ 对象链接库是否属于动态型别的?
提示 #1:当所有东西都衍生自单一的根类别(root class),通常叫做 "Object"。
提示 #2:当容器类别 container classes,像 List﹑Stack﹑Set 等,都不是
template 版的。
提示 #3:当容器类别(List﹑Stack﹑Set 等)把插入/取出的元素,都视为指向
"Object" 的指针时。(你可以把 Apple 放进容器中,但当你取出时,编
译器只知道它是衍生自 Object,所以你得用指针转型将它转回 Apple* ;
你最好祈祷它真的是个 Apple,否则你会脑充血的。)
你可用 "dynamic_cast"(于 1994 年才加入的)来使指针转型「安全些」,但这种
动态测试依旧是“动态”的。这种程序风格是 C++ 动态型别的基本要素,你可以呼
叫函数:「把这个 Object 转换成 Apple,或是给我个 NULL,如果它不是 Apple的
话」,你就得到动态型别了:直到执行时期才知道会发生什么事。
若你用 template 去实作出容器类别,C++ 编译器会静态侦测出 99% 的型态信息(
"99%" 并不是真的;有些人宣称能做到 100%,而那些需要持续性 (persistence) 的
人,只能得到低于 100% 的静态型别检验)。重点是:C++ 透过 template 来做到泛
型(genericity),而非透过继承。
========================================
Q93:在 C++ 里怎样用继承?它和 Smalltalk 有何不同?
有些人认为继承是用来重用程序代码的。在 C++ 中,这是不对的。说明白点,「继承
不是『为』重用程序代码而设计的。」
【译注】这一个分野相当重要。否则,C++ 使用者就会感染「继承发烧症」
(inheritance fever)。
C++ 继承的目的是用来表现接口一致性(产生子类别),而不是重用程序代码。C++ 中
,重用程序代码通常是靠「成份」(composition) 而非继承。换句话说,继承主要是用
来当作「特异化」(specialization) 的技术,而非实作上的技巧。
这是与 Smalltalk 主要的不同之处,在 Smalltalk 里只有一种继承的型式(C++ 有
"private" 继承--「共享程序代码,但不承袭其接口」,有 "public" 继承--表现
"kind-of" 关系)。Smalltalk 语言非常(相对于只是程序的习惯)允许你置放一个
override 覆盖(它会去呼叫个「我看不懂」的运作行为),以达到「隐藏住」继承
下来的运作行为的“效果”。更进一步,Smalltalk 可让观念界的 "is-a" 关系“独
立于”子类别阶层之外(子型别不必也是子类别;譬如,你可以让某个东西是一个
Stack,却不必继承自 Stack 类别)。
相反的,C++ 对继承的限制更严:没办法不用到继承就做出“观念上的 is-a”关系
(有个 C++ 的解决方法:透过 ABC 来分离接口与实作)。C++ 编译器利用公共继承
额外附的语意信息,以提供静态型别。
C++语言常见问题解
== Part 3/4 ============================
comp.lang.c++ Frequently Asked Questions list (with answers, fortunately).
Copyright (C) 1991-96 Marshall P. Cline, Ph.D.
Posting 3 of 4.
Posting #1 explains copying permissions, (no)warranty, table-of-contents, etc
=============================
■□ 第14节:程序风格指导
=============================
Q81:有任何好的 C++ 程序写作的标准吗?
感谢您阅读这份文件,而不是再发明自己的一套。
但是请不要在 comp.lang.c++ 里问这问题。几乎所有软件工程师,或多或少都把这
种东西看成是「大玩具」。而且,一些想成为 C++ 程序撰写标准的东西,是由那些
不熟悉这语言及方法论的人弄出来的,所以最后它只能成为「过去式」的标准。这种
「摆错位置」的现象,让大家对程序写作标准产生不信任感。
很明显的,在 comp.lang.c++ 问这问题的人,是想使自己更精进,不会因自己的无
知而绊倒,然而一些回答却只是让情况更糟而已。
========================================
Q82:程序撰写标准是必要的吗?有它就够了吗?
程序撰写标准不会让不懂 OO 的人变懂;只有训练及经验才有可能。如果它有用处的
话,那就是抑制住那些琐碎无关紧要的程序片段--当大机构想把零散的程序设计组
织整合起来时,这些片段常常会出现。
但事实上你要的不光是这种标准而已。它们提供的架构让新手少去担心一些自由度,
但是系统化的方法论会比这些好看的标准做得更好。组织机构需要的是一致性的设计
与实行“哲学”,譬如:强型别或弱型别?用指针还是参考接口? stream I/O 还是
stdio? C++ 程序该不该呼叫 C 的?反过来呢? ABC 该怎么用?继承该用为实作的
技巧还是特异化的技巧?该用哪一种测试策略?一一去检查吗?该不该为每个资料成
员都提供一致的 "get" 和 "set" 接口?接口该由外往内还是由内往外设计?错误状
况该用 try/catch/throw 还是传回值来处理?……等等。
我们需要的是详细的“设计”部份的「半标准」。我推荐一个三段式标准:训练﹑谘
询顾问以及链接库。训练乃提供「密集教学」,咨询顾问让 OO 观念深刻化,而非仅
仅是被教过而已,高品质的链接库则是提供「长程的教学」。上述三种培训都有很热
门的市场景况。(【译注】无疑的,这是指美﹑加地区。)接受过上述培训的组织都
有如此的忠告:「买现成的吧,不要自己硬干 (Buy, Don't Build.)。」买链接库,
买训练课程,买开发工具,买咨询顾问。想靠自学来达到成功的工具厂商及应用/系
统厂商,都会发现成功很困难。
【译注】这一段十分具有参考价值。不过有些背景资料得提供给各位参考。别忘了:
作者是美国人,是以该地为背景,且留意一下他所服务的公司是做什么的..
... 唉!国内有这么多的专业顾问公司吗? :-<
少数人会说:程序撰写标准只是「理想」而已,但在上述的组织机构中,它仍有其必
要性。
底下的 FAQs 提供一些基本的指导惯例及风格。
========================================
Q83:我们的组织该以以往 C 的经验来决定程序撰写标准吗?
No!
不论你的 C 经验有多丰富,不论你有多高深的 C 能力,好的 C 程序员并不会让你
直接就成为好的 C++ 程序员。从 C 移到 C++ 并不仅是学习 "++" 的语法语意而已
,一个组织想达到 OOP 的境界,却未将 "OO" 的精神放进 OOP 里的话,只是自欺罢
了;会计的资产负债表会把他们的愚蠢显现出来。
C++ 程序撰写标准应该由 C++ 专家来调整,不妨先在 comp.lang.c++ 里头问问题(
但是不要用 "coding standard" 这种字眼;只要这样子问:「这种技巧有何优缺点
?」)。找个能帮你避开陷阱的高手,上个训练课程,买链接库,看看「好的」程序
库是否合乎你的程序撰写标准。绝对不要光靠自己来制定标准,除非你对它已有某种
程度的掌握。没有标准总比有烂标准好,因为不恰当的「官方说法」会让不够聪明的
平民难以追随。现在 C++ 训练课程及链接库,已有十分兴盛的市场。
再提一件事:当某个东西炙手可热时,招摇撞骗者亦随之而生;务必三思而后行。也
要问一下从某处修过课的人,因为老手不见得也是个好教员。最后,选个懂得指导别
人的从业人员,而不是个对此语言/方法论只有过时知识的全职教师。
【译注】善哉斯言!
========================================
Q84:我该在函数中间或是开头来宣告区域变量?
在第一次用到它的地方附近。
对象在宣告的时候就会被初始化(被建构)。如果在初始化对象的地方没有足够的资
讯,直到函数中间才有的话,你可以在开头处初始个「空值」给它,等以后再「设定
」其值;你也可以在函数中间再初始个正确的东西给它。以执行效率来说,一开始就
让它有正确的值,会比先建立它,搞一搞它,之后再重建它来得好。以像 "String"
这种简单的例子来看,会有 350% 的速度差距。在你的系统上可能会不同;当然整个
系统可能不会降低到 300+%,但是“一定”会有不必要的性能衰退现象。
常见的反驳是:「我们会替对象的每个资料提供 "set" 运作行为,则建构时的额外
耗费就会分散开来。」这比效能负荷更糟,因为你添加了维护的梦靥。替每个资料提
供 "set" 运作行为就等于对资料不设防:你把内部实作技巧都显露出来了。你隐藏
到的只有成员对象的实体“名字”而已,但你用到的 List﹑String 和 float(举例
来说)型态都曝光了。通常维护会比 CPU 执行时间耗费的资源更多。
区域变量应该在靠近它第一次用到之处宣告。很抱歉,这和 C 老手的习惯不同,但
是「新的」不见得就是「不好的」。
========================================
Q85:哪一种原始档命名惯例最好? "foo.C"? "foo.cc"? "foo.cpp"?
如果你已有个惯例,就用它吧。如果没有,看看你的编译器,看它用的是哪一种。典
型的答案是:".C", ".cc", ".cpp", 或 ".cxx"(很自然的,".C" 扩展名是假设该
档案系统会区分出 ".C" ".c" 大小写)。
在 Paradigm Shift 公司,我们在 Makefiles 里用 ".C",即使是在不区分大小写的
档案系统下(在有区分的系统中,我们用一个编译器选项:「假设 .c 档案都是 C++
的程序」;譬如:IBM CSet++ 用 "-Tdp",Zortech C++ 用 "-cpp",Borland C++用
"-P",等等)。
========================================
Q86:哪一种标头档命名惯例最好? "foo.H"? "foo.hh"? "foo.hpp"?
如果你已有个惯例,就用它吧。如果没有,而且你的编辑器不必去区分 C 和 C++ 檔
案的话,只要用 ".h" 就行了,否则就用编辑器所要的,像 ".H"﹑".hh" 或是
".hpp"。
在 Paradigm Shift 公司,我们用 ".h" 做为 C 和 C++ 的源文件(然后,我们就
不再建那些纯粹的 C 标头档案)。
========================================
Q87:C++ 有没有像 lint 那样的指导原则?
Yes,有一些常见的例子是危险的。
但是它们都不尽然是「坏的」,因为有些情况下,再差的例子也得用上去。
* "Fred" 类别的设定运操作数应该传回 "*this",当成是 "Fred&"(以允许成串的设
定指令)。
* 有任何虚拟函数的类别,都该有个虚拟解构子。
* 若一个类别有 {解构子,设定运操作数,拷贝建构子} 其一的话,通常三者也都全
部需要。
* "Fred" 类别的拷贝建构子和设定运操作数,都该将它们的参数加上 "const":分别
是 "Fred::Fred(const Fred&)" 和 "Fred& Fred::operator=(const Fred&)" 。
* 类别的子对象一定要用初始化串行 (initialization lists) 而不要用设定的方
式,因为对使用者自订类别而言,会有很大的效率差距(3x!)。
* 许多设定运操作数都应该先测试:「我们」是不是「他们」;譬如:
Fred& Fred::operator= (const Fred& fred)
{
if (this == &fred) return *this;
//...normal assignment duties...
return *this;
}
有时候没必要测试,但一般说来,这些情况都是:没有必要由使用者提供外显的
设定运操作数的时候(相对于编译器提供的设定运操作数)。
* 在那些同时定义了 "+="﹑"+" 及 "=" 的类别中,"a+=b" 和 "a=a+b" 通常应该
做同样的事;其它类似的内建运操作数亦同(譬如:a+=1 和 ++a; p[i] 和 *(p+i);
等等)。这可使用二元运操作数 "op=" 之型式来强制做到;譬如:
Fred operator+ (const Fred& a, const Fred& b)
{
Fred ans = a;
ans += b;
return ans;
}
这样一来,有「建构性」的二元运算甚至可以不是伙伴。但常用的运操作数有时可
能会更有效率地实作出来(譬如,如果 "Fred" 类别本来就是个 "String",且
"+=" 必须重新配置/拷贝字符串内存的话,一开始就知道它的最后长度,可能会
比较好)。
==============================================
■□ 第15节:Smalltalk 程序者学习 C++ 之钥
==============================================
Q88:为什么 C++ 的 FAQ 有一节讨论 Smalltalk?这是用来攻击 Smalltalk 的吗?
世界上「主要的」两个 OOPLs 是 C++ 与 Smalltalk。由于这个流行的 OOPL 已有第
二大的使用者总数量,许多新的 C++ 程序者是由 Smalltalk 背景跳过来的。这一节
会回答以下问题:
* 这两个语言的差别?
* 从 Smalltalk 跳到 C++ 的程序者,要知道些什么,才能精通 C++?
这一节 *!*不会*!* 回答这些问题:
* 哪一种语言「最好」?
* 为什么 Smalltalk「很烂」?
* 为什么 C++「很烂」?
这可不是对 Smalltalk 恐怖份子挑衅,让他们趁我熟睡时戳我的轮胎(在我很难得
有空休息的这段时间内 。
========================================
Q89:C++ 和 Smalltalk 的差别在哪?
最重要的不同是:
* 静态型别或动态型别?
* 继承只能用于产生子型别上?
* 数值语意还是参考语意 (value vs reference semantics)?
头两个差异会在这一节中解释,第三点则是下一节的讨论主题。
如果你是 Smalltalk 程序者,现在想学 C++,底下三则 FAQs 最好仔细研读。
========================================
Q90:什么是「静态型别」?它和 Smalltalk 有多相似/不像?
静态型别(static typing)是说:编译器会“静态地”(于编译时期)检验各运算
的型态安全性,而不是产生执行时才会去检查的程序代码。例如,在静态型别之下,会
去侦测比对函数自变量的型态签名,不正确的配对会被编译器挑出错误来,而非在执行
时才被挑出。
OO 的程序里,最常见的「型态不符」错误是:欲对某对象激活个成员函数,但该物
件并未准备好要处理该运算动作。譬如,如果 "Fred" 类别有成员函数 "f()" 但没
有 "g()",且 "fred" 是 "Fred" 类别的案例,那么 "fred.f()" 就是合法的,
"fred.g()" 则是非法的。C++(静态地)在编译期捕捉型别错误,Smalltalk 则(动
态地)在执行期捕捉。(技术上,C++ 很像 Pascal--“半”静态型别--因为指
标转型与 union 都能用来破坏型别系统;这提醒了我们:你用指针转型与 union 的
频率,只能像你用 "goto" 那样。)
========================================
Q91:「静态型别」与「动态型别」哪一种比较适合 C++?
若你想最有效率使用 C++,请把她当成静态型别语言来用。
C++ 极富弹性,你可以(藉由指针转型﹑union 或 #define)让她「长得」像
Smalltalk。但是不要这样做。这提醒了我们:少用 #define。
有些场合,指针转型和 union 是必要的,甚至是很好的做法,但须谨慎为之。指针
转型等于是叫编译器完全信赖你。错误的指针转型可能会毁坏堆积﹑在别的对象记忆
体中乱搞﹑呼叫不存在的运作行为﹑造成一般性错误(general failure)。这是很
糟糕的事。如果你避免用与这些相关的东西,你的 C++ 程序会更安全﹑更快,因为
能在编译期就检测的东西,就不必留到执行期再做。
就算你喜欢动态型别,也请避免在 C++ 里使用,或者请考虑换另一个将型态检查延
迟到执行期才做的语言。C++ 将型态检验 100% 都放在编译时期;她没有任何执行期
型态检验的内建机制。如果你把 C++ 当成一个动态型别的 OOPL 来用,你的命运将
操之汝手。
========================================
Q92:怎样分辨某个 C++ 对象链接库是否属于动态型别的?
提示 #1:当所有东西都衍生自单一的根类别(root class),通常叫做 "Object"。
提示 #2:当容器类别 container classes,像 List﹑Stack﹑Set 等,都不是
template 版的。
提示 #3:当容器类别(List﹑Stack﹑Set 等)把插入/取出的元素,都视为指向
"Object" 的指针时。(你可以把 Apple 放进容器中,但当你取出时,编
译器只知道它是衍生自 Object,所以你得用指针转型将它转回 Apple* ;
你最好祈祷它真的是个 Apple,否则你会脑充血的。)
你可用 "dynamic_cast"(于 1994 年才加入的)来使指针转型「安全些」,但这种
动态测试依旧是“动态”的。这种程序风格是 C++ 动态型别的基本要素,你可以呼
叫函数:「把这个 Object 转换成 Apple,或是给我个 NULL,如果它不是 Apple的
话」,你就得到动态型别了:直到执行时期才知道会发生什么事。
若你用 template 去实作出容器类别,C++ 编译器会静态侦测出 99% 的型态信息(
"99%" 并不是真的;有些人宣称能做到 100%,而那些需要持续性 (persistence) 的
人,只能得到低于 100% 的静态型别检验)。重点是:C++ 透过 template 来做到泛
型(genericity),而非透过继承。
========================================
Q93:在 C++ 里怎样用继承?它和 Smalltalk 有何不同?
有些人认为继承是用来重用程序代码的。在 C++ 中,这是不对的。说明白点,「继承
不是『为』重用程序代码而设计的。」
【译注】这一个分野相当重要。否则,C++ 使用者就会感染「继承发烧症」
(inheritance fever)。
C++ 继承的目的是用来表现接口一致性(产生子类别),而不是重用程序代码。C++ 中
,重用程序代码通常是靠「成份」(composition) 而非继承。换句话说,继承主要是用
来当作「特异化」(specialization) 的技术,而非实作上的技巧。
这是与 Smalltalk 主要的不同之处,在 Smalltalk 里只有一种继承的型式(C++ 有
"private" 继承--「共享程序代码,但不承袭其接口」,有 "public" 继承--表现
"kind-of" 关系)。Smalltalk 语言非常(相对于只是程序的习惯)允许你置放一个
override 覆盖(它会去呼叫个「我看不懂」的运作行为),以达到「隐藏住」继承
下来的运作行为的“效果”。更进一步,Smalltalk 可让观念界的 "is-a" 关系“独
立于”子类别阶层之外(子型别不必也是子类别;譬如,你可以让某个东西是一个
Stack,却不必继承自 Stack 类别)。
相反的,C++ 对继承的限制更严:没办法不用到继承就做出“观念上的 is-a”关系
(有个 C++ 的解决方法:透过 ABC 来分离接口与实作)。C++ 编译器利用公共继承
额外附的语意信息,以提供静态型别。