📄 00000001.htm
字号:
车“是一种”(is-a-kind-of-a) 车子,福特汽车“有”(has-a) 引擎、轮胎……等 <BR>等零件。「部份」的层次随著 ADT 的流行,已成为软体系统的一份子了;而「继承 <BR>」则添入了“另一个”重要的软体分解角度。 <BR> <BR>======================================== <BR> <BR>Q52:怎样在 C++ 中表现出继承? <BR> <BR>用 ": public" 语法: <BR> <BR> class Car : public Vehicle { <BR> //^^^^^^^^---- ": public" 读作「是一种」("is-a-kind-of-a") <BR> //... <BR> }; <BR> <BR>我们以几种方式来描述上面的关系: <BR> <BR> * Car 是「一种」("a kind of a") Vehicle <BR> * Car 乃「衍生自」("derived from") Vehicle <BR> * Car 是个「特异化的」("a specialized") Vehicle <BR> * Car 是 Vehicle 的「子类别」("subclass") <BR> * Vehicle 是 Car 的「基底类别」("base class") <BR> * Vehicle 是 Car 的「父类别」("superclass") (这不是 C++ 界常用的说法) <BR> 【译注】"superclass" 是 Smalltalk 语言的关键字。 <BR> <BR>======================================== <BR> <BR>Q53:把衍生类别的指标转型成指向它的基底,可以吗? <BR> <BR>可以。 <BR> <BR>衍生类别是该基底类别的特异化版本(衍生者「是一种」("a-kind-of") 基底)。这 <BR>种向上的转换是绝对安全的,而且常常会发生(如果我指向一个汽车 Car,实际上我 <BR>是指向一个车子 Vehicle): <BR> <BR> void f(Vehicle* v); <BR> void g(Car* c) { f(c); } //绝对很安全;不需要转型 <BR> <BR>注意:在这里我们假设的是 "public" 的继承;後面会再提到「另一种」"private/ <BR>protected" 的继承。 <BR> <BR>======================================== <BR> <BR>Q54:Derived* --> Base* 是正常的;那为什麽 Derived** --> Base** 则否? <BR> <BR>C++ 让 Derived* 能转型到 Base*,是因为衍生的物件「是一种」基底的物件。然而 <BR>想由 Derived** 转型到 Base** 则是错误的!要是能够的话,Base** 就可能会被解 <BR>参用(产生一个 Base*),该 Base* 就可能指向另一个“不一样的”衍生类别,这 <BR>是不对的。 <BR> <BR>照此看来,衍生类别的阵列就「不是一种」基底类别的阵列。在 Paradigm Shift 公 <BR>司的 C++ 训练课程里,我们用底下的例子来比喻: <BR> <BR> "一袋苹果「不是」一袋水果". <BR> "A bag of apples is NOT a bag of fruit". <BR> <BR>如果一袋苹果可以当成一袋水果来传递,别人就可能把香蕉放到苹果袋里头去! <BR> <BR>======================================== <BR> <BR>Q55:衍生类别的阵列「不是」基底的阵列,是否表示阵列不好? <BR> <BR>没错,「阵列很烂」(开玩笑的 :-) 。 <BR> <BR>C++ 内建的阵列有一个不易察觉的问题。想一想: <BR> <BR> void f(Base* arrayOfBase) <BR> { <BR> arrayOfBase[3].memberfn(); <BR> } <BR> <BR> main() <BR> { <BR> Derived arrayOfDerived[10]; <BR> f(arrayOfDerived); <BR> } <BR> <BR>编译器认为这完全是型别安全的,因为由 Derived* 转换到 Base* 是正常的。但事 <BR>实上这很差劲:因为 Derived 可能会比 Base 还要大,f() 里头的阵列索引不光是 <BR>没有型别安全,甚至还可能没指到真正的物件呢!通常它会指到某个倒楣的 <BR>Derived 物件的中间去。 <BR> <BR>根本的问题在於:C++ 不能分辨出「指向一个东西」和「指向一个阵列」。很自然的 <BR>,这是 C++“继承”自 C 语言的特徵。 <BR> <BR>注意:如果我们用的是一个像阵列的「类别」而非最原始的阵列(譬如:"Array<T>" <BR>而非 "T[]"),这问题就可以在编译期被挑出来,而非在执行的时候。 <BR> <BR>========================== <BR>● 12A:继承--虚拟函数 <BR>========================== <BR> <BR>Q56:什麽是「虚拟成员函数」? <BR> <BR>虚拟函数可让衍生的类别「取代」原基底类别所提供的运作。只要某物件是衍生出来 <BR>的,就算我们是透过基底物件的指标,而不是以衍生物件的指标来存取该物件,编译 <BR>器仍会确保「取代後」的成员函数被呼叫。这可让基底类别的演算法被衍生者所替换 <BR>,即使我们不知道衍生类别长什麽样子。 <BR> <BR>注意:衍生的类别亦可“部份”取代(覆盖,override)掉基底的运作行为(如有必 <BR>要,衍生类别的运作行为亦可呼叫它的基底类别版本)。 <BR> <BR>======================================== <BR> <BR>Q57:C++ 怎样同时做到动态系结和静态型别? <BR> <BR>底下的讨论中,"ptr" 指的是「指标」或「参考」。 <BR> <BR>一个 ptr 有两种型态:静态的 ptr 型态,与动态的「被指向的物件」的型态(该物 <BR>件可能实际上是个由其他类别衍生出来的类别的 ptr)。 <BR> <BR>「静态型别」("static typing") 是指:该呼叫的「合法性」,是以 ptr 的静态型 <BR>别为侦测之依据,如果 ptr 的型别能处理成员函数,则「指向的物件」自然也能。 <BR> <BR>「动态系结」("dynamic binding") 是指:「程式码」呼叫是以「被指向的物件」之 <BR>型态为依据。被称为「动态系结」,是因为真正会被呼叫的程式码是动态地(於执行 <BR>时期)决定的。 <BR> <BR>======================================== <BR> <BR>Q58:衍生类别能否将基底类别的非虚拟函数覆盖(override)过去? <BR> <BR>可以,但不好。 <BR> <BR>C++ 的老手有时会重新定义非虚拟的函数,以提升效率(换一种可能会运用到衍生类 <BR>别才有的资源的作法),或是用以避开遮蔽效应(hiding rule,底下会提,或是看 <BR>看 ARM ["Annotated Reference Manual"] sect.13.1),但是用户的可见性效果必 <BR>须完全相同,因为非虚拟的函数是以指标/参考的静态型别为分派(dispatch)的依 <BR>据,而非以指到的/被参考到的物件之动态型别来决定。 <BR> <BR>======================================== <BR> <BR>Q59:"Warning: Derived::f(int) hides Base::f(float)" 是什麽意思? <BR> <BR>这是指:你死不了的。 <BR> <BR>你出的问题是:如果 Derived 宣告了个叫做 "f" 的成员函数,Base 却早已宣告了 <BR>个不同型态签名型式(譬如:参数型态或是 const 不同)的 "f",这样子 Base "f" <BR>就会被「遮蔽 hide」住,而不是被「多载 overload」或「覆盖 override」(即使 <BR>Base "f" 已经是虚拟的了)。 <BR> <BR>解决法:Derived 要替 Base 被遮蔽的成员函数重新定义(就算它不是虚拟的)。通 <BR>常重定义的函数,仅仅是去呼叫合适的 Base 成员函数,譬如: <BR> <BR> class Base { <BR> public: <BR> void f(int); <BR> }; <BR> <BR> class Derived : public Base { <BR> public: <BR> void f(double); <BR> void f(int i) { Base::f(i); } <BR> }; // ^^^^^^^^^^--- 重定义的函数只是去呼叫 Base::f(int) <BR> <BR>======================== <BR>● 12B:继承--一致性 <BR>======================== <BR> <BR>Q60:我该遮蔽住由基底类别继承来的公共成员函数吗? <BR> <BR>绝对绝对绝对绝对不要这样做! <BR> <BR>想去遮蔽(删去、撤消)掉继承下来的公共成员函数,是个很常见的错误。这通常是 <BR>脑袋塞满了浆糊的人才会做的傻事。 <BR> <BR>======================================== <BR> <BR>Q61:圆形 "Circle" 是一种椭圆 "Ellipse" 吗? <BR> <BR>若椭圆能够不对称地改变其两轴的大小,则答案就是否定的。 <BR> <BR>比方说,椭圆有个 "setSize(x,y)" 的运作行为,且它保证说「椭圆的 width() 为 <BR>x,height() 为 y」。这种情况之下,正圆形就不能算是一种椭圆。因为只要把某个 <BR>椭圆能做而正圆形不能的东西放进去,圆形就不再是个椭圆了。 <BR> <BR>这样一来,圆和椭圆之间可能有两种的(合法)关系: <BR> * 将圆与椭圆完全分开来谈。 <BR> * 让圆及椭圆都同时自一个基底衍生出来,该基底为「不能做不对称的 setSize <BR> 运作的特殊椭圆形」。 <BR> <BR>以第一个方案而言,椭圆可继承自「非对称图形」(伴随著一个 setSize(x,y) ), <BR>圆形则继承自「对称图形」,带有一个 setSize(size) 成员函数。 <BR>
⌨️ 快捷键说明
复制代码
Ctrl + C
搜索代码
Ctrl + F
全屏模式
F11
切换主题
Ctrl + Shift + D
显示快捷键
?
增大字号
Ctrl + =
减小字号
Ctrl + -