注册 | 登录
收藏 | 帮助
热门文章
编辑推荐
相关文章  
模块复用——c++类、dll和com
获取ACCESS2000数据库中的所有表
C++语言常见问题解答(1)
C++语言常见问题解答(3)
C++语言常见问题解答(4)
在c++程序中重启自己的一种方法
您现在的位置: 顶尖设计 >> IT学院 >> 编程开发 >> C >> 文章正文
C++语言常见问题解答(2)
作者:中译者:叶秉哲  来源:永远的UNIX  点击:  更新:2006-12-19
简介:
======== 
 
Q51:何时该用继承? 
 
做为一个「特异化」(specialization) 的机制。 
 
人类以两种角度来抽象化事物:「部份」(part-of) 和「种类」(kind-of)。福特汽 
车“是一种”(is-a-kind-of-a) 车子,福特汽车“有”(has-a) 引擎、轮胎……等 
等零件。「部份」的层次随著 ADT 的流行,已成为软体系统的一份子了;而「继承 
」则添入了“另一个”重要的软体分解角度。 
 
======================================== 
 
Q52:怎样在 C++ 中表现出继承? 
 
用 ": public" 语法: 
 
        class Car : public Vehicle { 
                //^^^^^^^^---- ": public" 读作「是一种」("is-a-kind-of-a") 
          //... 
        }; 
 
我们以几种方式来描述上面的关系: 
 
 * Car 是「一种」("a kind of a") Vehicle 
 * Car 乃「衍生自」("derived from") Vehicle 
 * Car 是个「特异化的」("a specialized") Vehicle 
 * Car 是 Vehicle 的「子类别」("subclass") 
 * Vehicle 是 Car 的「基底类别」("base class") 
 * Vehicle 是 Car 的「父类别」("superclass") (这不是 C++ 界常用的说法) 
   【译注】"superclass" 是 Smalltalk 语言的关键字。 
 
======================================== 
 
Q53:把衍生类别的指标转型成指向它的基底,可以吗? 
 
可以。 
 
衍生类别是该基底类别的特异化版本(衍生者「是一种」("a-kind-of") 基底)。这 
种向上的转换是绝对安全的,而且常常会发生(如果我指向一个汽车 Car,实际上我 
是指向一个车子 Vehicle): 
 
        void f(Vehicle* v); 
        void g(Car* c) { f(c); }        //绝对很安全;不需要转型 
 
注意:在这里我们假设的是 "public" 的继承;後面会再提到「另一种」"private/ 
protected" 的继承。 
 
======================================== 
 
Q54:Derived* --> Base* 是正常的;那为什麽 Derived** --> Base** 则否? 
 
C++ 让 Derived* 能转型到 Base*,是因为衍生的物件「是一种」基底的物件。然而 
想由 Derived** 转型到 Base** 则是错误的!要是能够的话,Base** 就可能会被解 
参用(产生一个 Base*),该 Base* 就可能指向另一个“不一样的”衍生类别,这 
是不对的。 
 
照此看来,衍生类别的阵列就「不是一种」基底类别的阵列。在 Paradigm Shift 公 
司的 C++ 训练课程里,我们用底下的例子来比喻: 
 
               "一袋苹果「不是」一袋水果". 
               "A bag of apples is NOT a bag of fruit". 
 
如果一袋苹果可以当成一袋水果来传递,别人就可能把香蕉放到苹果袋里头去! 
 
======================================== 
 
Q55:衍生类别的阵列「不是」基底的阵列,是否表示阵列不好? 
 
没错,「阵列很烂」(开玩笑的 :-) 。 
 
C++ 内建的阵列有一个不易察觉的问题。想一想: 
 
        void f(Base* arrayOfBase) 
        { 
          arrayOfBase[3].memberfn(); 
        } 
 
        main() 
        { 
          Derived arrayOfDerived[10]; 
          f(arrayOfDerived); 
        } 
 
编译器认为这完全是型别安全的,因为由 Derived* 转换到 Base* 是正常的。但事 
实上这很差劲:因为 Derived 可能会比 Base 还要大,f() 里头的阵列索引不光是 
没有型别安全,甚至还可能没指到真正的物件呢!通常它会指到某个倒楣的 
Derived 物件的中间去。 
 
根本的问题在於:C++ 不能分辨出「指向一个东西」和「指向一个阵列」。很自然的 
,这是 C++“继承”自 C 语言的特徵。 
 
注意:如果我们用的是一个像阵列的「类别」而非最原始的阵列(譬如:"Array" 
而非 "T[]"),这问题就可以在编译期被挑出来,而非在执行的时候。 
 
========================== 
● 12A:继承--虚拟函数 
========================== 
 
Q56:什麽是「虚拟成员函数」? 
 
虚拟函数可让衍生的类别「取代」原基底类别所提供的运作。只要某物件是衍生出来 
的,就算我们是透过基底物件的指标,而不是以衍生物件的指标来存取该物件,编译 
器仍会确保「取代後」的成员函数被呼叫。这可让基底类别的演算法被衍生者所替换 
,即使我们不知道衍生类别长什麽样子。 
 
注意:衍生的类别亦可“部份”取代(覆盖,override)掉基底的运作行为(如有必 
要,衍生类别的运作行为亦可呼叫它的基底类别版本)。 
 
======================================== 
 
Q57:C++ 怎样同时做到动态系结和静态型别? 
 
底下的讨论中,"ptr" 指的是「指标」或「参考」。 
 
一个 ptr 有两种型态:静态的 ptr 型态,与动态的「被指向的物件」的型态(该物 
件可能实际上是个由其他类别衍生出来的类别的 ptr)。 
 
「静态型别」("static typing") 是指:该呼叫的「合法性」,是以 ptr 的静态型 
别为侦测之依据,如果 ptr 的型别能处理成员函数,则「指向的物件」自然也能。 
 
「动态系结」("dynamic binding") 是指:「程式码」呼叫是以「被指向的物件」之 
型态为依据。被称为「动态系结」,是因为真正会被呼叫的程式码是动态地(於执行 
时期)决定的。 
 
======================================== 
 
Q58:衍生类别能否将基底类别的非虚拟函数覆盖(override)过去? 
 
可以,但不好。 
 
C++ 的老手有时会重新定义非虚拟的函数,以提升效率(换一种可能会运用到衍生类 
别才有的资源的作法),或是用以避开遮蔽效应(hiding rule,底下会提,或是看 
看 ARM ["Annotated Reference Manual"] sect.13.1),但是用户的可见性效果必 
须完全相同,因为非虚拟的函数是以指标/参考的静态型别为分派(dispatch)的依 
据,而非以指到的/被参考到的物件之动态型别来决定。 
 
======================================== 
 
Q59:"Warning: Derived::f(int) hides Base::f(float)" 是什麽意思? 
 
这是指:你死不了的。 
 
你出的问题是:如果 Derived 宣告了个叫做 "f" 的成员函数,Base 却早已宣告了 
个不同型态签名型式(譬如:参数型态或是 const 不同)的 "f",这样子 Base "f" 
就会被「遮蔽 hide」住,而不是被「多载 overload」或「覆盖 override」(即使 
Base "f" 已经是虚拟的了)。 
 
解决法:Derived 要替 Base 被遮蔽的成员函数重新定义(就算它不是虚拟的)。通 
常重定义的函数,仅仅是去呼叫合适的 Base 成员函数,譬如: 
 
        class Base { 
        public: 
          void f(int); 
        }; 
 
        class Derived : public Base { 
        public: 
          void f(double); 
          void f(int i) { Base::f(i); } 
        };             // ^^^^^^^^^^--- 重定义的函数只是去呼叫 Base::f(int) 
 
======================== 
● 12B:继承--一致性 
======================== 
 
Q60:我该遮蔽住由基底类别继承来的公共成员函数吗? 
 
绝对绝对绝对绝对不要这样做! 
 
想去遮蔽(删去、撤消)掉继承下来的公共成员函数,是个很常见的错误。这通常是 
脑袋塞满了浆糊的人才会做的傻事。 
 
======================================== 
 
Q61:圆形 "Circle" 是一种椭圆 "Ellipse" 吗? 
 
若椭圆能够不对称地改变其两轴的大小,则答案就是否定的。 
 
比方说,椭圆有个 "setSize(x,y)" 的运作行为,且它保证说「椭圆的 width() 为 
x,height() 为 y」。这种情况之下,正圆形就不能算是一种椭圆。因为只要把某个 
椭圆能做而正圆形不能的东西放进去,圆形就不再是个椭圆了。 
 
这样一来,圆和椭圆之间可能有两种的(合法)关系: 
 * 将圆与椭圆完全分开来谈。 
 * 让圆及椭圆都同时自一个基底衍生出来,该基底为「不能做不对称的 setSize 
   运作的特殊椭圆形」。 
 
以第一个方案而言,椭圆可继承自「非对称图形」(伴随著一个 setSize(x,y) ), 
圆形则继承自「对称图形」,带有一个 setSize(size) 成员函数。 
 
第二个方案中,可让卵形 "Oval" 类别有个 "setSize(size)":将 "width()" 和 
"height()" 都设成 "size",然後让椭圆和圆形都自卵形中衍生出来。椭圆(而不是 
正圆形)会加入一个 "setSize(x,y)" 运算(如果这个 "setSize()" 运作行为的名 
称重复了,就得注意前面提过的「遮蔽效应」)。 
 
======================================== 
 
Q62:对「圆形是/不是一种椭圆」这两难问题,有没有其他说法? 
 
如果你说:椭圆都可以不对称地挤压,又说:圆形是一种椭圆,又说:圆形不能不对 
称地挤压下去,那麽很明显的,你说过的某句话要做修正(老实说,该取消掉)。所 
以你不是得去掉 "Ellipse::setSize(x,y)",去掉圆形和椭圆间的继承关系,就是得 
承认你的「圆形」不一定是正圆。 
 
这儿有两个 OO/C++ 新手最易落入的陷阱。他们想用程式小技巧来弥补差劲的事前设 
计(他们重新定义 Circle::setSize(x,y),让它丢出一个例外,呼叫 "abort()" , 
或是选用两参数的平均数,或是不做任何事情),不幸的,这些技俩都会让使用者感 
到吃惊:他们原本都预期 "width() == x" 和 "height() == y" 这两个事实会成立。 
 
唯一合理的做法似乎是:降低椭圆形 "setSize(x,y)" 的保证事项(譬如,你可以改 
成:「这运作行为“可能”会把 width() 设成 x、height() 设成 y,也可能“不做 
任何事”」)。不幸的,这样会把界限冲淡,因为使用者没有任何有意义的物件行为 
足以依靠,整个类别阶层也就无毫价值可言了(很难说服别人去用一个:问你说它是 
做什麽的,你却只会耸耸肩膀说不知道的物件)。 
 
========================== 
● 12C:继承--存取规则 
========================== 
 
Q63:为什麽衍生的类别无法存取基底的 "private" 东西? 
 
让你不被基底类别将来的改变所影响。 
 
衍生类别不能存取到基底的私有(private)成员,它有效地把衍生类别「封住」, 
基底类别内的私有成员如有改变,也不会影响到衍生的类别。 
 
======================================== 
 
Q64:"public:"、"private:"、"protected:" 的差别是? 
 
"Private:" 在前几节中讨论过了;"public:" 是指:「任何人都能存取之」;第三 
个 "protected:" 是让某成员(资料成员或是成员函数)只能由衍生类别存取之。 
 
【译注】"protected:" 是让「衍生类别」,而非让「衍生类别的物件案例」能存取 
        得到 protected 的部份。 
 
======================================== 
 
Q65:当我改变了内部的东西,怎样避免子类别被破坏? 
 
物件类别有两个不同的介面,提供给不同种类的用户: 
 * "public:" 介面用以服务不相关的类别。 
 * "protected:" 介面用以服务衍生的类别。 
 
除非你预期所有的子类别都会由你们的工作小组建出来,否则你应该将基底类别的资 
料位元内容放在 "private:" 处,用 "protected:" 行内存取函数来存取那些资料。 
这样的话,即使基底类别的私有资料改变了,衍生类别的程式也不会报废,除非你改 
变了基底类别的 protected 处的存取函数。 
 
================================ 
● 12D:继承--建构子与解构子 
================================ 
 
Q66:若基底类别的建构子呼叫一个虚拟函数,为什麽衍生类别覆盖掉的那个虚拟函 
     数却不会被呼叫到? 
 
在基底类别 Base 的建构子执行过程中,该物件还不是属於衍生 Derived 的,所以 
如果 "Base::Base()" 呼叫了虚拟函数 "virt()",则 "Base::virt()" 会被呼叫, 
即使真的有 "Derived::virt()"。 
 
类似的道理,当 Base 的解构子执行时,该物件不再是个 Derived 了,所以当 
Base::~Base() 呼叫 "virt()",则 "Base::virt()" 会被执行,而非覆盖後的版本 
"Derived::virt()"。 
 
当你想像到:如果 "Derived::virt()" 碰得到 Derived 类别的物件成员,会造成什 
麽样的灾难,你很快就会看出这规则的明智之处。 
 
================================ 
 
Q67:衍生类别的解构子应该外显地呼叫基底的解构子吗? 
 
不要,绝对不要外显地呼叫解构子(「绝对不要」指的是「几乎完全不要」)。 
 
衍生类别的解构子(不管你是否明显定义过)会“自动”去呼叫成员物件的、以及基 
底类别之子物件的解构子。成员物件会以它们在类别中出现的相反顺序解构,接下来 
是基底类别的子物件,以它们出现在类别基底列表的相反顺序解构之。 
 
只有在极为特殊的情况下,你才应外显地呼叫解构子,像是:解构一个由「新放入的 
new 运算子」配置的物件。 
 
=========================================== 
● 12E:继承--Private 与 protected 继承 
=========================================== 
 
Q68:该怎麽表达出「私有继承」(private inheritance)? 
 
用 ": private" 来代替 ": public."  譬如: 
 
        class Foo : private Bar { 
          //... 
        }; 
 
================================ 
 
Q69:「私有继承」和「成份」(composition) 有多类似? 
 
私有继承是「成份」(has-a) 的一种语法变形。 
 
譬如:「汽车有引擎」("car has-a engine") 关系可用成份来表达: 
 
        class Engine { 
        public: 
          Engine(int numCylinders); 
          void start();                 //starts this Engine 
        }; 
 
        class Car { 
        public: 
          Car() : e_(8) { }             //initializes this Car with 8 cylinders 
          void start() { e_.start(); }  //start this Car by starting its engine 
        private: 
          Engine e_; 
        }; 
 
同样的 "has-a" 关系也可用私有继承来表达: 
 
        class Car : private Engine { 
        public: 
          Car() : Engine(8) { }         //initializes this Car with 8 cylinders 
          Engine::start;                //start this Car by starting its engine 
        }; 
 
这两种型式的成份有几分相似性: 
 * 这两种情况之下,Car 只含有一个 Engine 成员物件。 
 * 两种情况都不能让(外界)使用者由 Car* 转换成 Engine* 。 
 
也有几个不同点: 
 * 如果你想要让每个 Car 都含有数个 Engine 的话,就得用第一个型式。 
 * 第二个型式可能会导致不必要的多重继承(multiple inheritance)。 
 * 第二个型式允许 Car 的成员从 Car* 转换成 Engine* 。 
 * 第二个型式可存取到基底类别的 "protected" 成员。 
 * 第二个型式允许 Car 覆盖掉 Engine 的虚拟函数。 
 
注意:私有继承通常是用来获得基底类别 "protected:" 成员的存取权力,但这通常 
只是个短程的解决方案。 
 
======================================== 
 
Q70:我比较该用哪一种:成份还是私有继承? 
 
成份。 
 
正常情形下,你不希望存取到太多其他类别的内部,但私有继承会给你这些额外的权 
力(与责任)。不过私有继承不是洪水猛兽;它只是得多花心力去维护罢了,因为它 
增加了别人动到你的东西、让你的程式出差错的机会。 
 
合法而长程地使用私有继承的时机是:当你想新建一个 Fred 类别,它会用到 Wilma 
类别的程式码,而且 Wilma 的程式码也会呼叫到你这个 Fred 类别里的运作行为时 
。这种情形之下,Fred 呼叫了 Wilma 的非虚拟函数,Wilma 也呼叫了它自己的、会 
被 Fred 所覆盖的虚拟函数(通常是纯虚拟函数)。要用成份来做的

上一页  [1] [2] [3] 下一页






  • 上一篇文章:
  • 下一篇文章:
  • 分享此文:该页面添加到 Mister Wong 添加到雅虎Yahoo!收藏 Add to:Del.icio.us Post to Furl Digg this 添加到Google书签 reddit spurl blogmarks 365Key 评论  收藏  分享  打印
     我来说两句
    姓名:       验证码:   
    主页: 
    评分: 1分 2分 3分 4分 5分
    本频道近期热评文章:
      关于我们 | 联系我们 | 站点地图 | 广告投放 | 友情链接 | 在线留言 | 版权申明
    版权所有 © 2004-2007 顶尖设计(bobd.cn)
    未经授权禁止转载,摘编,复制本站内容或建立镜像. 沪ICP备07504942号 
    网络110
    报警服务