| 热门文章 |
 |
|
| 编辑推荐 |
 |
|
|
|
|
|
作者:中译者:叶秉哲
来源:永远的UNIX 点击: 更新:2006-12-19
|
|
因为想把过去的习惯“ 遗忘”,会比一开始就学习静态型别来得困难。 【译注】作者是以「语言学习」的角度来看的。事实上,若先有 Smalltalk 之类的 物件导向观念的背景知识,再来学 C++ 就不必再转换 "paradigm"--物件 导向的中心思维是不会变的,变的只是实行细节而已。 ======================================== Q96:什麽是 NIHCL?到哪里拿到它? NIHCL 代表 "national-institute-of-health's-class-library",美国国家卫生局 物件程式库。取得法:anonymous ftp 到 [128.231.128.7], 档案:pub/nihcl-3.0.tar.Z 。 NIHCL(有人念作 "N-I-H-C-L",有人念作 "nickel")是个由 Smalltalk 转移过来 的 C++ 物件程式库。有些 NIHCL 用到的动态型别很棒(譬如:persistent objects ,持续性物件),也有些地方动态型别会和 C++ 语言的静态型别相冲突,造成紧张 关系。 详见前面关於 Smalltalk 的 FAQs。 =============================== ■□ 第16节:参考与数值语意 =============================== Q97:什麽是数值以及参考语意?哪一种在 C++ 里最好? 在参考语意 (reference semantics) 中,「设定」是个「指标拷贝」的动作(也就 是“参考”这个词的本意),数值语意 (value semantics,或 "copy" semantics) 的设定则是真正地「拷贝其值」,而不是做指标拷贝的动作。C++ 让你选择:用设定 运算子来拷贝其值(copy/value 语意),或是用指标拷贝方式来拷贝指标 (reference 语意)。C++ 让你能覆盖掉 (override) 设定运算子,让它去做你想要 的事,不过系统预设的(而且是最常见的)方式是拷贝其「数值」。 参考语意的优点:弹性、动态系结(在 C++ 里,你只能以传指标或传参考来达到动 态系结,而不是用传值的方式)。 数值语意的优点:速度。对需要物件(而非指标)的场合来说,「速度」似乎是很奇 怪的特点,但事实上,我们比较常存取物件本身,较不常去拷贝它。所以偶尔的拷贝 所付出的代价,(通常)会被拥有「真正的物件本身」、而非仅是指向物件的指标所 带来的效益弥补过去。 有三个情况,你会得到真正的物件,而不是指向它的指标:区域变数、整体/静态变 数、完全被某类别包含在内 (fully contained) 的成员物件。这里头最重要的就是 最後一个(也就是「成份」)。 後面的 FAQs 会有更多关於 copy-vs-reference 语意的资讯,请全部读完,以得到 较平衡的观点。前几则会刻意偏向数值语意,所以若你只读前面的,你的观点就会有 所偏颇。 设定 (assignment) 还有别的事项(譬如:shallow vs deep copy)没在这儿提到。 ======================================== Q98:「虚拟资料」是什麽?怎麽样/为什麽该在 C++ 里使用它? 虚拟资料让衍生类别能改变基底类别的物件成员所属的类别。严格说来,C++ 并不「 支援」虚拟资料,但可以模拟出来。不漂亮,但还能用。 欲模拟之,基底类别必须有个指标指向成员物件,衍生类别必须提供一个 "new" 到 的物件,以让原基底类别的指标所指到。该基底类别也要有一个以上正常的建构子, 以提供它们自己的参考(也是透过 "new"),且基底类别的解构子也要 "delete" 掉 被参考者。 举例来说,"Stack" 类别可能有个 Array 成员物件(采用指标),衍生类别 "StretchableStack" 可能会把基底类别的成员资料 "Array" 覆盖成 "StretchableArray"。想做到的话,StretchableArray 必须继承自 Array,这样子 Stack 就会有个 "Array*"。Stack 的正常建构子会用 "new Array" 来初始化它的 "Array*",但 Stack 也会有一个(可能是在 "protected:" 里)特别的建构子,以 自衍生类别中接收一个 "Array*"; StretchableArray 的建构子会用 "new StretchableArray" 把它传给那个特别的建构子。 优点: * 容易做出 StretchableStack(大部份的程式都继承下来了)。 * 使用者可把 StretchableStack 当成“是一种”Stack 来传递。 缺点: * 多增加额外的间接存取层,才能碰到 Array。 * 多增加额外的自由记忆体配置负担(new 与 delete)。 * 多增加额外的动态系结负担(原因请见下一则 FAQ)。 换句话说,在“我们”这一边,很轻松就成功做出 StretchableStack,但所有用户 却都为此付出代价。不幸的,额外负荷不仅在 StretchableStack 会有,连 Stack 也会。 请看下下一则 FAQ,看看使用者会「付出」多少代价。也请读读下一则 FAQ 以後的 几则(不看其他的,你将得不到平衡的报导)。 ======================================== Q99:虚拟资料和动态资料有何差别? 最容易分辨出来的方法,就是看看颇为类似的「虚拟函数」。虚拟成员函数是指:在 所有子类别中,它的宣告(型态签名)部份必须保持不变,但是定义(本体)的部份 可以被覆盖(override)。继承下来的成员函数可被覆盖,是子类别的静态性质 (static property);它不随任何物件之生命期而动态地改变,同一个子类别的不同 物件,也不可能会有不同的成员函数的定义。 现在请回头重读前面这一段,但稍作些代换: * 「成员函数」 --> 「成员物件」 * 「型态签名」 --> 「型别」 * 「本体」 --> 「真正所属的类别」 这样子,你就看到虚拟资料的定义。 从另一个角度来看,就是把「各个物件」的成员函数与「动态」成员函数区分开来。 「各个物件」成员函数是指:在任何物件案例中,该成员函数可能会有所不同,可能 会塞入函数指标来实作出来;这个指标可以是 "const",因为它在物件生命期中不会 变更。「动态」成员函数是指:该成员函数会随时间动态地改变;也可能以函数指标 来做,但该指标不会是 const 的。 推而广之,我们得到三种不同的资料成员概念: * 虚拟资料:成员物件的定义(真正所属的类别)可被子类别覆盖,只要它的宣告 (型别)维持不变,且此覆盖是子类别的静态性质。 * 各物件的资料:任何类别的物件在初始化时,可以案例化不同型式(型别)的成 员物件(通常是一个 "wrapper" 包起来的物件),且该成员物件真正所属的类别 ,是把它包起来的那个物件之静态性质。 * 动态资料:成员物件真正所属的类别,可随时间动态地改变。 它们看起来都差不多,是因为 C++ 都不「直接支援」它们,只是「能做得出来」而 已;在这种情形下,模拟它们的机制也都一样:指向(可能是抽象的)基底类别的指 标。在直接提供这些 "first class" 抽象化机制的语言中,这三者间的差别十分明 显,它们各有不同的语法。 ======================================== Q100:我该正常地用指标来配置资料成员,还是该用「成份」(composition)? 成份。 正常情况下,你的成员资料应该被「包含」在合成的物件里(但也不总是如此; "wrapper" 物件就是你会想用指标/参考的好例子;N-to-1-uses-a 的关系也需要某 种指标/参考之类的东西)。 有三个理由说明,完全被包含的成员物件(「成份」)的效率,比自由配置物件的指 标还要好: * 额外的间接层,每当你想存取成员物件时。 * 额外的动态配置("new" 於建构子中,"delete" 於解构子中)。 * 额外的动态系结(底下会解释)。 ======================================== Q101:动态配置成员物件有三个效率因素,它们的相对代价是多少? 这三个效率因素,上一则 FAQ 有列举出来: * 以它本身而言,额外的间接层影响不大。 * 动态配置可能是个效率问题(当有许多配置动作时,典型的 malloc 会拖慢速度 ;OO 软体会被动态配置拖垮,除非你事先就留意到它了)。 * 用指标而非物件的话,会带来额外的动态系结。每当 C++ 编译器能知道某物件「 真正的」类别,该虚拟函数呼叫就能“静态”地系结住,能够被 inline。Inline 可能有无限大的 (但你可能只会相信有半打的 :-) 最佳化机会,像是 procedural integration、暂存器生命期等等事项。三种情形之下,C++ 编译器能知道物件真 正的类别:区域变数、整体/静态变数、完全被包含的成员物件。 完全被包含的成员物件,可达到很大的最佳化效果,这是「成员物件的指标」所不可 能办到的。这也就是为什麽采用参考语意的语言,会「与生俱来」就效率不彰的原因 了。 注意:请读读下面三则 FAQs 以得到平衡的观点! ======================================== Q102:"inline virtual" 的成员函数真的会被 "inline" 吗? Yes,可是... 一个透过指标或参考的 virtual 呼叫总是动态决定的,可能永远都不会被 inline。 原因:编译器直到执行时(亦即:动态地),才会知道该呼叫哪个程式,因为那一段 程式,可能会来自呼叫者编译过後才出现的衍生类别。 因此,inline virtual 的呼叫可被 inline 的唯一时机是:编译器有办法知道某物 件「真正所属的类别」之时,这是虚拟函数呼叫里所要知道的事情。这只会发生在: 编译器拥有真正的物件,而非该物件的指标或参考。也就是说:不是区域变数、整体 /静态物件,就是合成物件里的完全包含物件。 注意:inline 和非 inline 的差距,通常会比正常的和虚拟的函数呼叫之差别更为 显著。譬如,正常的与虚拟的函数呼叫,通常只差两个记忆体参考的动作而已,可是 inline 与非 inline 函数就会有一个数量级的差距(与数万次影响不大的成员函数 呼叫相比,函数没有用 inline virtual 的话,会造成 25X 的效率损失! [Doug Lea, "Customization in C++," proc Usenix C++ 1990]). 针对此现象的对策:不要陷入编译器/语言厂商之间,对彼此产品的虚拟函数呼叫, 做永无止尽的性能比较争论(或是广告噱头!)之中。和语言/编译器能否将成员函 数呼叫做「行内展开」相比,这种比较完全没有意义。也就是说,许多语言编译器厂 商,拼命强调他们的函数分派方式有多好,但如果他们没做“行内”成员函数呼叫的 话,整体性能还是会很差,因为 inline--而非分派--才是最重要的性能影响因 素。 注意:请读读下两则 FAQs 以看看另一种说法! ======================================== Q103:看起来我不应该用参考语意了,是吗? 错。 参考语意是个好东西。我们不能抛弃指标,我们只要不让软体的指标变成一个大老鼠 窝就行了。在 C++ 里,你可以选择该用参考语意(指标/参考)还是数值语意(物 件真正包含其他物件)的时机。在大型系统中,两者应该取得平衡。然而如果你全都 用指标来做的话,速度会大大的降低。 接近问题层次的物件,会比较高阶的物件还要大。这些针对「问题空间」抽象化的个 体本身,通常比它们内部的「数值」更为重要。参考语意应该用於问题空间的物件上 。 注意:问题空间的物件,通常会比解题空间的更为高阶抽象化,所以相对地问题空间 的物件通常会有较少的交谈性。因此 C++ 给我们一个“理想的”解决法:我们用参 考语意,来对付那些需要独立的个体识别 (identity) 者,或是大到不适合直接拷贝 的物件;其他情形则可选择数值语意。因此,使用频率较高的就用数值语意,因为( 只有)在不造成伤害的场合下,我们才去增加弹性;必要时,我们还是选择效率! 还有其他关於实际 OO 设计方面的问题。想精通 OO/C++ 得花时间,以及高素质的训 练。若你想有个强大的工具,你必须投资下去。 <<<< 还不要停下来! 请一并读读下一则 FAQ!! >>>> ======================================== Q104:参考语意效率不高,那麽我是否应该用传值呼叫? 不。 前面的 FAQ 是讨论“成员物件”(member object) 的,而不是函数参数。一般说来 ,位於继承阶层里的物件,应该用参考或指标来传递,而非传值,因为惟有如此你才 能得到(你想要的)动态系结(传值呼叫和继承不能安全混用,因为如果把大大的子 类别物件当成基底的物件来传值的话,它会被“切掉”)。 除非有足以令人信服的反方理由,否则成员物件应该用数值,而参数该用参考传递。 前几则 FAQs 提到一些「足以信服的理由」,以支持“成员物件该用参考”一事了上一页 [1] [2]
|
|
|