C++的不足之处讨论系列(一):虚拟函数(转贴)

VisualStudio 2002-12-03 10:00:06
作者:Ian Joyner, 译者:cber
以下文章翻译自Ian Joyner所著的
C++?? A Critique of C++ and Programming and Language Trends of the 1990s 3/E[Ian Joyner 1996]
征得Ian Joyner本人的同意,我得以将该文翻译成中文.因此,本文的中文版权应该属于我;-)
该文章的英文及中文版本都用于非商业用途,你可以随意地复制和转贴它.不过最好在转贴它时加上我的这段声明.
如有人或机构想要出版该文,请最好联系原著版权所有人及我.
该篇文章已经包含在Ian Joyner所写的Objects Unencapsulated一书中
Ian Joyner的联系方式: i.joyner@acm.org
我的联系方式: cber@email.com.cn
前言[译者所写的]:要想彻底的掌握一种语言,不但需要知道它的长处有哪些,而且需要知道它的不足之处又有哪些.这样我们才能用好这门语言,也才能说我们自己掌握了这门语言.
在所有对C++的批评中,虚拟函数这一部分是最复杂的.这主要是由于C++中复杂的机制所引起的.虽然本篇文章认为多态(polymorphism)是实现面向对象编程(OOP)的关键特性,但还是请你不要对此观点(即虚拟函数机制是C++中的一大败笔)感到有什么不安,继续看下去,如果你仅仅想知道一个大概的话,那么你也可以跳过此节.[译者注:建议大家还是看看这节会比较好].
在C++中,当子类改写/重定义(override/redefine)了在父类中定义了的函数时,关键字virtual使得该函数具有了多态性,但是virtual关键字也并不是必不可少的(只要在父类中被定义一次就行了).编译器通过产生动态分配(dynamic dispatch)的方式来实现真正的多态函数调用.
这样,在C++中,问题就产生了:如果设计父类的人员不能预见到子类可能会改写哪个函数,那么子类就不能使得这个函数具有多态性.这对于C++来说是一个很严重的缺陷,因为它减少了软件组件(software components)的弹性(flexibility),从而使得写出可重用及可扩展的函数库也变得困难起来.
C++同时也允许函数的重载(overload),在这种情况下,编译器通过传入的参数来进行正确的函数调用.在函数调用时所引用的实参类型必须吻合被重载的函数组(overloaded functions)中某一个函数的形参类型.重载函数与重写函数(具有多态性的函数)的不同之处在于:重载函的调用是在编译期间就被决定了,而重写函数的调用则是在运行期间被决定的.
当一个父类被设计出来时,程序员只能猜测子类可能会重载/重写哪个函数.子类可以随时重载任何一个函数,但这种机制并不是多态.为了实现多态,设计父类的程序员必须指定一个函数为virtual,这样会告诉编译器在类的跳转表(class jump table)[译者窃以为是vtable,即虚拟函数入口表]中建立一个分发入口.于是,对于决定什么事情是由编译器自动完成,或是由其他语言的编译器自动完成这个重任就放到了程序员的肩上.这些都是从最初的C++的实现中继承下来的,而和一些特定的编译器及联结器无关.
对于重写,我们有着三种不同的选择,分别对应于:“千万别”,“可以”及“一定要”重写:
1.重写一个函数是被禁止的.子类必须使用已有的函数;
2.函数可以被重写.子类可以使用已有的函数,也可以使用自己写的函数,前提是这个函数必须遵循最初的界面定义,而且实现的功能尽可能的少及完善;
3.函数是一个抽象的函数.对于该函数没有提供任何的实现,每个子类都必须提供其各自的实现.
父类的设计者必须要决定1和3中的函数,而子类的设计者只需要考虑2就行了.对于这些选择,程序语言必须要提供直接的语法支持.

选项1
C++并不能禁止在子类中重写一个函数.即使是被声明为private virtual的函数也可以被重写.[Sakkinen92]中指出了即使在通过其他方法都不能访问到private virtual函数,子类也可以对其进行重写.[译者注:Sakkinen92我也没看过,但经我简单的测试,确实可以在子类中重写父类中的private virtual函数]
实现这种选择的唯一方法就是不要使用虚拟函数,但是这样的话,函数就等于整个被替换掉了.首先,函数可能会在无意中被子类的函数给替换掉.在同一个scope中重新宣告一个函数将会导致名字冲突(name clash);编译器将会就此报告出一个“duplicate declaration”的语法错误.允许两个拥有同名的实体存在于同一个scope中将会导致语义的二义性(ambiguity)及其他问题(可参见于name overloading这节).
下面的例子阐明了第二个问题:
class A
{
public:
void nonvirt();
virtual void virt();
};
class B : public A
{
public:
void nonvirt();
void virt();
};
A a;
B b;
A *ap = &b;
B *bp = &b;
bp->nonvirt();
//calls B::nonvirt as you would expect
ap->nonvirt();
//calls A::nonvirt even though this object is of type B
ap->virt();
//calls B::virt, the correct version of the routine for B objects
在这个例子里,B扩展或替换掉了A中的函数.B::nonvirt是应该被B的对象调用的函数.在此处我们必须指出,C++给客户端程序员(即使用我们这套继承体系架构的程序员)足够的弹性来调用A::nonvirt或是B::nonvirt,但我们也可以提供一种更简单,更直接的方式:提供给A::nonvirt和B::nonvirt不同的名字.这可以使得程序员能够正确地,显式地调用想要调用的函数,而不是陷入了上面的那种晦涩的,容易导致错误的陷阱中去.具体方法如下:
class B: public A
{
public:
void b_nonvirt();
void virt();
};
B b;
B *bp = &b;
bp->nonvirt();
//calls A::nonvirt
bp->b_nonvirt();
//calls B::b_nonvirt
现在,B的设计者就可以直接的操纵B的接口了.程序要求B的客户端(即调用B的代码)能够同时调用A::nonvirt和B::nonvirt,这点我们也做到了.就Object-Oriented Design(OOD)来说,这是一个不错的做法,因为它提供了健壮的接口定义(strongly defined interface)[译者认为:即不会引起调用歧义的接口].C++允许客户端程序员在类的接口处卖弄他们的技巧,借以对类进行扩展.在上例中所出现的就是设计B的程序员不能阻止其他程序员调用A::nonvirt.类B的对象拥有它们自己的nonvirt,但是即便如此,B的设计者也不能保证通过B的接口就一定能调用到正确版本的nonvirt.
C++同样不能阻止系统中对其他处的改动不会影响到B.假设我们需要写一个类C,在C中我们要求nonvirt是一个虚拟的函数.于是我们就必须回到A中将nonvirt改为虚拟的.但这又将使得我们对于B::nonvirt所玩弄的技巧又失去了作用(想想看,为什么:D).对于C需要一个virtual的需求(将已有的nonvirtual改为virtual)使得我们改变了父类,这又使得所有从父类继承下来的子类也相应地有了改变.这已经违背了OOP拥有低耦合的类的理由,新的需求,改动应该只产生局部的影响,而不是改变系统中其他地方,从而潜在地破坏了系统的已有部分.
另一个问题是,同样的一条语句必须一直保持着同样的语义.例如:对于诸如a->f()这样的多态性语句的解释,系统调用的是由最符合a所真正指向类型的那个f(),而不管对象的类型到底是A,还是A的子类.然而,对于C++的程序员来说,他们必须要清楚地了解当f()被定义成virtual或是non-virtual时,a->f()的真正涵义.所以,语句a->f()不能独立于其实现,而且隐藏的实现原理也不是一成不变的.对于f()的宣告的一次改变将会相应地改变调用它时的语义.与实现独立意味着对于实现的改变不会改变语句的语义,或是执行的语义.
如果在宣告中的改变导致相应的语义的改变,编译器应该能检测到错误的产生.程序员应该在宣告被改变的情况下保持语义的不变.这反映了软件开发中的动态特性,在其中你将能发现程序文本的永久改变.
其他另一个与a->f()相应的,语义不能被保持不变的例子是:构造函数(可参考于C++ ARM, section 10.9c, p 232).而Eiffel和Java则不存在这样的问题.它们中所采用的机制简单而又清晰,不会导致C++中所产生的那些令人吃惊的现象.在Java中,所有的一起都是虚拟的,为了让一个方法[译者注:对应于C++的函数]不能被重写,我们可以用final修饰符来修饰这个方法.
Eiffel允许程序员指定一个函数为frozen,在这种情况下,这个函数就不能在子类中被重写.

选项2
是使用现有的函数还是重写一个,这应该是由撰写子类的程序员所决定的.在C++中,要想拥有这种能力则必须在父类中指定为virtual.对于OOD来说,你所决定不想作的与你所决定想作的同样重要,你的决定应该是越迟下越好.这种策略可以避免错误在系统前期就被包含进去.你作决定越早,你就越有可能被以后所证明是错误的假设所包围;或是你所作的假设在一种情况下是正确的,然而在另一种情况下却会出错,从而使得你所写出来的软件比较脆弱,不具有重用性(reusable)[译者注:软件的可重用性对于软件来说是一个很重要的特性,具体可以参考《Object-Oriented Software Construct》中对于软件的外部特性的叙述,P7, Reusability, Charpter 1.2 A REVIEW OF EXTERNAL FACTORS].
C++要求我们在父类中就要指定可能的多态性(这可以通过virtual来指定),当然我们也可以在继承链中的中间的类导入virtual机制,从而预先判断某个函数是否可以在子类中被重定义.这种做法将导致问题的出现:如那些并非真正多态的函数(not actually polymorphic)也必须通过效率较低的table技术来被调用,而不像直接调用那个函数来的高效[译者注:在文章的上下文中并没有出现not actually polymorphic特性的确切定义,根据我的理解,应该是声明为polymorphic,而实际上的动作并没能体现polymorphic这样的一种特性].虽然这样做并不会引起大量的花费(overhead),但我们知道,在OO程序中经常会出现使用大量的,短小的,目标单一明确的函数,如果将所有这些都累计下来,也会导致一个相当可观的花费.C++中的政策是这样的:需要被重定义的函数必须被声明为virtual.糟糕的是,C++同时也说了,non-virtual函数不能被重定义,这使得设计使用子类的程序员就无法对于这些函数拥有自己的控制权.[译者注:原作中此句显得有待推敲,原文是这样写的:it says that non-virtual routines cannot be redefined, 我猜测作者想表达的意思应该是:If you have defined
...全文
27 14 打赏 收藏 转发到动态 举报
写回复
用AI写文章
14 条回复
切换为时间正序
请发表友善的回复…
发表回复
baifeng 2002-12-04
  • 打赏
  • 举报
回复
g z
baifeng 2002-12-03
  • 打赏
  • 举报
回复
g z
lbaby 2002-12-03
  • 打赏
  • 举报
回复
ok
收藏了
SHIZUMARU 2002-12-03
  • 打赏
  • 举报
回复
to VisualStudio(嗷~~~) :

精神可嘉。

多了解一些其他的语言,尤其是像Eiffel这样典雅的语言,绝对是有好处的。从这个角度上来说,Joyner的这些文章还是值得感谢。不过,拿Eiffel或者OOSC那一套往C++头上硬套,也不一定合适,所以我说“不要当真”。

既然你这么喜欢Joyner的这些言论,想来对Eiffel会有兴趣。去看看OOSC也许会有帮助。(OOSC:Object-Oriented Software Construction 2nd Edition,清华大学出版社影印版)
VisualStudio 2002-12-03
  • 打赏
  • 举报
回复
to SHIZUMARU(绯雨闲丸)
我是刚刚知道的,他对我来说就不能算是冷饭.而且既然别人挑出毛病来了,我们应该感谢别人.至于你说"Joyner这个人,基本上还是Eiffel的fan",我不了解,不过用惯了某种工具,碰到其他的工具,喜欢做个比较这也是人之常情,就象你说的"姑妄挑之,你也就姑妄看之",但倘若别人挑的对,我们就该留心了,以后注意注意,避免出错.你说是不是?
SHIZUMARU 2002-12-03
  • 打赏
  • 举报
回复
to VisualStudio(嗷~~~) :

这个东西,一年多以前我就在C++ View上面看到过,你说这是不是炒冷饭?

www.c-view.org还有cber翻译的Joyner的其他文章,也可以去看看。Joyner这个人,基本上还是Eiffel的fan。他给C++挑的毛病,姑妄挑之,你也就姑妄看之,不要当真就好。
VisualStudio 2002-12-03
  • 打赏
  • 举报
回复
to KennyYuan
我赞同你的观点-----"学会接受现实并更好地利用它才是你的本领"
我也承认这个世界没有什么是完美的.
我贴这个帖子并没有贬低C++的意思, 我一直用C++, 以后只要C++没有被淘汰我也用.
我以前并不知道C++中有这样的不足, 我贴出来只是为了与大家共享, 为了和大家交流, 向高手学习.
有人看过,也有人没看过,看过的高手希望你们来指点一下,没看过的也可来交流交流.
KennyYuan 2002-12-03
  • 打赏
  • 举报
回复
翻译完了最好再转到CSDN来

KennyYuan 2002-12-03
  • 打赏
  • 举报
回复
我也看过了,而且看的是英文版的。

建议某些人翻译整个的comp.lang.c++

或者翻译整个的gotw.ca

或者整个的cuj.com

或者http://www.research.att.com/~bs/

KennyYuan 2002-12-03
  • 打赏
  • 举报
回复
我可以写一系列的《人的不足之处讨论系列》,可是有用么?

学会接受现实并更好地利用它才是你的本领。

我们说中国国情不好,经济环境不好,可是在这样的环境里面你就不生活不工作不挣钱了么?

批判留给设计者之间去讨论吧!我要去工作了

VisualStudio 2002-12-03
  • 打赏
  • 举报
回复
to SHIZUMARU
你看过不等于别人也看过,你既然早就看过,为什么不早点拿出来共享呢?
SHIZUMARU 2002-12-03
  • 打赏
  • 举报
回复
早就看过的东西,又拿出来炒冷饭,没劲。
cwanter 2002-12-03
  • 打赏
  • 举报
回复
并不是每个语言特性都是你要攀登的下一座山峰。使用语言的特性应该遵从应用的逻辑,而不是简单地因为它的存在就必须要使用它。---摘自 C++ Primer
ddmpqcw 2002-12-03
  • 打赏
  • 举报
回复
没有什么是完美的!

69,371

社区成员

发帖
与我相关
我的任务
社区描述
C语言相关问题讨论
社区管理员
  • C语言
  • 花神庙码农
  • 架构师李肯
加入社区
  • 近7日
  • 近30日
  • 至今
社区公告
暂无公告

试试用AI创作助手写篇文章吧