荔园在线

荔园之美,在春之萌芽,在夏之绽放,在秋之收获,在冬之沉淀

[回到开始] [上一篇][下一篇]


发信人: Second (石开), 信区: Program
标  题: Part of C++ FAQ Lite[21]——继承(完全继承和可置?
发信站: 荔园晨风BBS站 (Thu Jun  7 05:52:08 2001), 转信

译文出处:东日文档
[21] 继承 — 完全继承和可置换性
(part of c++ faq lite, copyright ? 1991-96, marshall cline, cline@parashift.
com)
----------------------------------------------------------------------------
----
faqs in section [21]:
[21.1] 我应该隐藏基类的公有成员函数吗?
[21.2] derived* —> base* 可以很好地工作; 为什么 derived** —> base** 不行?
[21.3] parking-lot-of-car(停车场)是一种 parking-lot-of-vehicle(交通工具停
泊场)吗?
[21.4] derived(派生类)数组是一种 base(基类)数组吗?
[21.5] 派生类数组“不是一种”基类数组是否意味着数组不好?
[21.6] 圆是一种椭圆吗?
[21.7] 对于“圆是/不是一种椭圆”这个两难问题,尤其它说法吗?
[21.8] 但我是数学博士,我相信圆是一种椭圆!这是否意味着marshall cline是傻瓜?

者c++是傻瓜?或者oo是傻瓜?
[21.9] 但我的问题与圆和椭圆无关,这种无聊的例子对我有什么好处?
----------------------------------------------------------------------------
----
[21.1] 我应该隐藏基类的公有成员函数吗?
不要,不要,不要这样做。永远不要!
试图隐藏(消除、废除、私有化)继承而来的公有成员函数是非常常见的设计错误。通

这产生于浆糊脑袋。
(注意: 本 faq 只论述公有继承有关; 私有和保护继承是不同的)
----------------------------------------------------------------------------
----
[21.2] derived* —> base* 可以很好地工作; 为什么 derived** —> base** 不行?
如果derived对象是一种base对象,则 c++允许derived* 转换成 base*。然而,将 der
ived**
转换成 base**将产生错误。尽管这个错误不是显而易见的,但未尝不是件好事。例如,
如果
你能够将 car**转换成 vehicle**,并且如果你能同样的将 nuclearsubmarine** 转换

vehicle**,那么你可能给这两个指针赋值,并最终使 car* 指针指向 nuclearsubmari
ne:
    class vehicle                           { /*...*/ };
    class car              : public vehicle { /*...*/ };
    class nuclearsubmarine : public vehicle { /*...*/ };
    main()
    {
      car   car;
      car*  carptr = &car;
      car** carptrptr = &carptr;
      vehicle** vehicleptrptr = carptrptr;  // 这在c++中是一个错误
      nuclearsubmarine  sub;
      nuclearsubmarine* subptr = ⊂
      *vehicleptrptr = subptr;
      // 最后这行将导致carptr指向 sub!
    }
换句话说,如果从derived** 到base**的转换是合法的,那么base**将可能被解除引用
(易变
的 base*),并且 base*可能被指向不同的派生类对象,这将导致严重的国家安全问题
(天知
道如果你调用了nuclearsubmarine(核潜艇)对象的 opengascap()成员函数会发生什么
!!而你
却认为这是一个car对象!
(注意: 本 faq 只论述公有继承有关; 私有和保护继承是不同的)
----------------------------------------------------------------------------
----
[21.3] parking-lot-of-car(停车场)是一种 parking-lot-of-vehicle(交通工具停
泊场)吗?
不。
我知道这听起来很奇怪,但这是事实。你可以将这看作为以上 faq的直接结论,或者你
可以
这样来理解:如果这个“是一种”关系成立的话,那么就可以将 parking-lot-of-vehi
cle类
型的指针指向一个 parking-lot-of-car。但是,parking-lot-of-vehicle有
addnewvehicletoparkinglot(vehicle&) 成员函数用来向停泊场添加任何vehicle(交通
工具)
对象。这样将允许你在 parking-lot-of-car(停车场)停泊一个nuclearsubmarine(核
潜艇)。
当然,当某人认为从 parking-lot-of-car删除一个car对象,而实际是一个nuclearsub
marine
时,他会非常惊讶。
用另一种方法阐述这个事实:一种事物的容器不是一种任何事物的容器。也许很难接受
,但
这是事实。
你可以不喜欢它,但必须接受它。
我们在oo/c++训练课程使用的最后一个例子:“一袋苹果不是一袋水果”。如果一袋苹
果能
够被传递给一袋水果的话,就可以把香蕉放入袋中,即使它被认为只能放苹果!
(注意: 本 faq 只论述公有继承有关; 私有和保护继承是不同的)
----------------------------------------------------------------------------
----
[21.4] derived(派生类)数组是一种 base(基类)数组吗?
不。
这是以上faq的结论。不幸的是它会把你带入困境,考虑一下这个:
    class base {
    public:
      virtual void f();             // 1
    };
    class derived : public base {
    public:
      // ...
    private:
      int i_;                       // 2
    };
    void usercode(base* arrayofbase)
    {
      arrayofbase[1].f();           // 3
    }
    main()
    {
      derived arrayofderived[10];   // 4
      usercode(arrayofderived);     // 5
    }
编译器会认为这是完美的类型安全。编号 5的这一行将 derived* 转换为 base*。但实
际上
这样做是可怕的:由于 derived比base 大,在编号3的这一行的指针运算是错误的:当
编译
器计算 arrayofbase[1]的地址时使用 sizeof(base),而数组其实是一个 derived数组
,这
意味着在编号3的这一行的所计算的地址(以及之后的成员函数f()的调用)并不在任何
对象
的起始位置!而在derived对象的中间。假设你的编译器使用通常的方法寻找虚函数,那

将导致第一个derived对象的 int i_ 被重新解释,将它看作指向虚函数表的指针,跟随

这个“指针”(意味着我们正在访问一个随机的内存位置),并将内存中那个位置的前
几个
字节解释为 c++成员函数的地址,然后将它们(随机的内存地址)装载到指令寄存器并
开始
从那个内存区产生机器指令。发生这样情况的几率相当高。
根本问题是 c++无法区别指向事物的指针和指向事物数组的指针。自然的,c++继承了c

这一特征。
注意:如果我们使用了类似数组(array-like)的类(例如,stl中的vector<derived>
)来
代替原始的数组,这个问题将会被作为编译时错误找出而不是运行时的灾难。
(注意: 本 faq 只论述公有继承有关; 私有和保护继承是不同的)
----------------------------------------------------------------------------
----
[21.5] 派生类数组“不是一种”基类数组是否意味着数组不好?
是的,数组很差劲。(开个玩笑)。
真诚的来说,数组和指针非常接近,并且指针很难处理。但是如果我们完全掌握了从设
计角
度来看,以上faq所说的为什么是一个问题(例如,如果你真的知道为什么事物的容器不
是一
种任何事物的容器),并且你认为将维护你的代码的其他人都网全掌握这些oo的设计事
实的
话,那么你可以自由使用数组。但是如果你象大多数人一样的话,你应该使用诸如vect
or<t>
这样的模板容器类而不是原始的数组。
(注意: 本 faq 只论述公有继承有关; 私有和保护继承是不同的)
----------------------------------------------------------------------------
----
[21.6] circle(圆)是一种 ellipse(椭圆)吗?
如果椭圆允许改变圆率,则不是。
例如,假设椭圆有一个setsize(x,y)成员函数,并且这个成员函数允许椭圆的 width()
是x,
height() 是y。在这种情况下,圆无法是一种椭圆。很简单,如果椭圆能做某些圆不能
做的
事,则圆不是一种椭圆。
据此推出圆和椭圆的两种(合法的)关系:
*使圆类和椭圆类完全无关
*使圆和椭圆都从一个基类派生,该基类是“不能执行不对称setsize()运算的椭圆”
在第一种情况下,椭圆可以从asymmetricshape(不对称图形)类派生,setsize(x,y)可

在asymmetricshape类中声明。而圆可以从有setsize(size)成员函数的symmetricshape
(对
称图形)类派生。
在第二种情况下,oval(卵形)类可以只有setsize(size)来同时设置width()(宽)和

height()(高)的大小。椭圆和圆都继承自oval。椭圆(但不是圆)可以增加setsize(
x,y)
运算(但如果setsize()成员函数名称重复,当心隐藏规则)
(注意: 本 faq 只论述公有继承有关; 私有和保护继承是不同的)
(注意: setsize(x,y) 并不是神圣的。依赖于你的目标,防止用户改变椭圆的尺寸也是
可以
的。在某些情况下,椭圆没有setsize(x,y)方法是有效的设计选择。然而这个系列的讨
论是
当你想为一个已存在的类建立一个派生类并且基类含有一个“无法接受”的方法时,该
如何
做。当然理想情形是在基类不存在时就发现这个问题。但生活并不总是理想的...)
----------------------------------------------------------------------------
----
[21.7] 对于“圆是/不是一种椭圆”这个两难问题,尤其它说法吗?
如果你声称所有椭圆是可以被压成不对称的,并且你声称圆是一种椭圆,并且你声称圆
不能
被压成不对称的。无疑你必须调整(实际上是撤回)你的声称之一。由此,你要么去掉

ellipse::setsize(x,y),去掉圆和椭圆的继承关系,要么承认你的circles(圆)不必
是正
圆。
这里有一个oo/c++编程新手通常会陷入的陷阱。他们会试图用代码的技巧来弥补设计的
缺陷
(他们重定义circle::setsize(x,y)来抛出异常,调用abort(),取两个参数的平均数,
或者
什么都不做)。不幸的是,由于用户期望 width() == x并且 height()==y,所以这些技
巧会
使用户惊讶。让用户惊讶是不允许的。
如果保持“圆是一种椭圆”的继承关系对你来说非常重要,那么你只能削弱椭圆的sets
ize(x,y)
所做的承诺。例如,你可以改变承诺为,“该城圆函数可以把 width()设置为x 和/或把

height()设置为y,或不做什么事情”。不幸的是由于用户没有任何意义的行为可以倚靠

这样会冲淡契约。因此整个层次都变得没有价值(如果某人问你到对象能做什么,而你
只能
耸耸肩膀的话,你很难说服他取使用这个对象)
(注意: 本 faq 只论述公有继承有关; 私有和保护继承是不同的)
(注意: setsize(x,y) 并不是神圣的。依赖于你的目标,防止用户改变椭圆的尺寸也是
可以
的。在某些情况下,椭圆没有setsize(x,y)方法是有效的设计选择。然而这个系列的讨
论是
当你想为一个已存在的类建立一个派生类并且基类含有一个“无法接受”的方法时,该
如何
做。当然理想情形是在基类不存在时就发现这个问题。但生活并不总是理想的...)
----------------------------------------------------------------------------
----
[21.8] 但我是数学博士,我相信圆是一种椭圆!这是否意味着marshall cline(译注:

本faq作者)是傻瓜?或者c++是傻瓜?或者oo是傻瓜?
事实上,这并不意味着这些。而是意味着你的直觉是错误的。
看,我收到并回复了大量的关于这个主题的热情的e-mail。我已经给各地上千个软件专
家讲
授了数百次。我知道它违反了你的直觉。但相信我,你的直觉是错误的。
真正的问题是你的直觉中的“是一种(kind of)”的概念不符合oo中的完全继承(学术

称为“子类型”)概念。派生类对象最起码必须是可以取代基类对象的。在圆/椭圆的情

下,setsize(x,y)成员函数违背了这个可置换性。
你有三个选择:[1]从ellipse(椭圆)类中删除 setsize(x,y)成员函数(从而废弃调用

setsize(x,y)成员函数的已存在代码),[2]允许circle(圆)的高和宽不同(一个不对

的圆),或者[3]去掉继承关系。抱歉,但没有其他选择。有人提过另一个选项,让圆和

椭圆都从第三个通用基类派生,但这只不过是以上选项[3]的变种罢了。
换一种说法就是,你要么使基类弱一些(在这里就是说你不能为椭圆的高和宽设置不同
的值),
要么使派生类强一些(在这里就是使圆同时具有对称的和不对称的能力)。
当这些都无法令人满意(就如圆/椭圆例子),通常就简单的消除继承关系。如果继承关

必须存在,你只能从基类中变形删除成员函数(setheight(y), setwidth(x), 和
setsize(x,y))
(注意: 本 faq 只论述公有继承有关; 私有和保护继承是不同的)
(注意: setsize(x,y) 并不是神圣的。依赖于你的目标,防止用户改变椭圆的尺寸也是
可以
的。在某些情况下,椭圆没有setsize(x,y)方法是有效的设计选择。然而这个系列的讨
论是
当你想为一个已存在的类建立一个派生类并且基类含有一个“无法接受”的方法时,该
如何
做。当然理想情形是在基类不存在时就发现这个问题。但生活并不总是理想的...)
----------------------------------------------------------------------------
----
[21.9] 但我的问题与圆和椭圆无关,这种无聊的例子对我有什么好处?
啊,有点小误会。你认为圆/椭圆例子是无聊的,但实际上,你的问题和它是同性质的。

我不在意你的继承问题是什么,但所有(是的,所有)不良的继承都可以归结为“圆不
是一
种椭圆”的例子。
这就是为什么:不良的继承总有一个有额外能力(经常是一个或两个额外的成员函数;
有时
是一个或多个成员函数给出的承诺)的基类,而派生类却无法满足它。你要么使基类弱
一些,
派生类强一些,要么消除继承关系。我见过很多很多很多不良的继承方案,相信我,它

都可以归结为圆/椭圆的例子。
因此,如果你真的理解了圆/椭圆的例子,你就能找出所有的不良继承。如果你没有理解

圆/椭圆问题,那么你很可能犯一些严重的并且昂贵的继承错误。
令人忧伤,但是真的。
(注意: 本 faq 只论述公有继承有关; 私有和保护继承是不同的)

--
                            既然热爱生命
                            那么,
                            一切都在意料之中。

※ 来源:·荔园晨风BBS站 bbs.szu.edu.cn·[FROM: 192.168.28.16]


[回到开始] [上一篇][下一篇]

荔园在线首页 友情链接:深圳大学 深大招生 荔园晨风BBS S-Term软件 网络书店