首页 > 编程开发 > C/C++ > 正文  
C++语言常见问题解答(2)
出自:中译者:叶秉哲 2002年01月24日 14:28
== Part 2/4  ============================ 

============================= 
■□ 第9节:自由记忆体管理 
============================= 
 
Q33:"delete p" 会删去 "p" 指标,还是它指到的资料,"*p" ? 
 
该指标指到的资料。 
 
"delete" 真正的意思是:「删去指标所指到的东西」(delete the thing pointed 
to by)。同样的英文误用也发生在 C 语言的「『释放』指标所指向的记忆体」上 
("free(p)" 真正的意思是:"free_the_stuff_pointed_to_by(p)" )。 
 
======================================== 
 
Q34:我能 "free()" 掉由 "new" 配置到的、"delete" 掉由 "malloc()" 配置到的 
     记忆体吗? 
 
不行。 
 
在同一个程式里,使用 malloc/free 及 new/delete 是完全合法、合理、安全的; 
但 free 掉由 new 配置到的,或 delete 掉由 malloc 配置到的指标则是不合法、 
不合理、该被痛骂一顿的。 
 
======================================== 
 
Q35:为什麽该用 "new" 而不是老字号的 malloc() ? 
 
建构子/解构子、型别安全性、可被覆盖(overridability)。 
 
建构子/解构子:和 "malloc(sizeof(Fred))" 不同,"new Fred()" 还会去呼叫 
Fred 的建构子。同理,"delete p" 会去呼叫 "*p" 的解构子。 
 
型别安全性:malloc() 会传回一个不具型别安全的 "void*",而 "new Fred()" 则 
会传回正确型态的指标(一个 "Fred*")。 
 
可被覆盖:"new" 是个可被物件类别覆盖的运算子,而 "malloc" 不是以「各个类别 
」作为覆盖的基准。 
 
======================================== 
 
Q36:为什麽 C++ 不替 "new" 及 "delete" 搭配个 "realloc()" ? 
 
避免你产生意外。 
 
当 realloc() 要拷贝配置区时,它做的是「逐位元 bitwise」的拷贝,这会弄坏大 
部份的 C++ 物件。不过 C++ 的物件应该要能自我拷贝才对:用它们自己的拷贝建构 
子或设定运算子。 
 
======================================== 
 
Q37:我该怎样配置/释放阵列? 
 
用 new[] 和 delete[] : 
 
        Fred* p = new Fred[100]; 
        //... 
        delete [] p; 
 
每当你在 "new" 运算式中用了 "[...]",你就必须在 "delete" 陈述中使用 "[]"。 
                                         ^^^^ 
这语法是必要的,因为「指向单一元素的指标」与「指向一个阵列的指标」在语法上 
并无法区分开来。 
 
======================================== 
 
Q38:万一我忘了将 "[]" 用在 "delete" 由 "new Fred[n]" 配置到的阵列,会发生 
     什麽事? 
 
灾难。 
 
这是程式者的--而不是编译器的--责任,去确保 new[] 与 delete[] 的正确配 
对。若你弄错了,编译器不会产生任何编译期或执行期的错误讯息。堆积(heap)被 
破坏是最可能的结局,或是更糟的,你的程式会当掉。 
 
======================================== 
 
Q39:成员函数做 "delete this" 的动作是合法的(并且是好的)吗? 
 
只要你小心的话就没事。 
 
我所谓的「小心」是: 
 
  1) 你得 100% 确定 "this" 是由 "new" 配置来的(而非 "new[]",亦非自订的 
     "new" 版本,一定要是最原始的 "new")。 
 
  2) 你得 100% 确定该成员函数是此物件最後一个会呼叫到的。 
 
  3) 做完自杀的动作 ("delete this;") 後,你不能再去碰 "this" 的物件了,包 
     括资料及运作行为在内。 
 
  4) 做完自杀的动作 ("delete this;") 後,你不能再去碰 "this" 指标了。 
     换句话说,你不能查看它、将它与其他指标或是 NULL 相比较、印出其值、 
     对它转型、对它做任何事情。 
 
很自然的,这项警告也适用於:当 "this" 是个指向基底类别的指标,而解构子不是 
virtual 的场合。 
 
======================================== 
 
Q40:我该怎麽用 new 来配置多维阵列? 
 
有很多方法,端视你对阵列大小的伸缩性之要求而定。极端一点的情形,如果你在编 
译期就知道所有阵列的维度,你可以静态地配置(就像 C 一样): 
 
        class Fred { /*...*/ }; 
 
        void manipulateArray() 
        { 
          Fred matrix[10][20]; 
 
          //使用 matrix[i][j]... 
 
          //不须特地去释放该阵列 
        } 
 
另一个极端情况,如果你希望该矩阵的每个小块都能不一样大,你可以在自由记忆体 
里配置之: 
 
        void manipulateArray(unsigned nrows, unsigned ncols[]) 
        //'nrows' 是该阵列之列数。 
        //所以合法的列数为 (0, nrows-1) 开区间。 
        //'ncols[r]' 则是 'r' 列的行数 ('r' 值域为 [0..nrows-1])。 
        { 
          Fred** matrix = new Fred*[nrows]; 
          for (unsigned r = 0; r  0; --r) 
            delete [] matrix[r-1]; 
          delete [] matrix; 
        } 
 
======================================== 
 
Q41:C++ 能不能做到在执行时期才指定阵列的长度? 
 
可以。STL 有一个 vector template 提供这种行为。请参考“程式库”一节的 STL 
项目。 
 
不行。内建的阵列型态必须在编译期就指定它的长度了。 
 
可以,内建的阵列可以在执行期才指定第一个索引的□围。譬如说,和上一则 FAQ 
相较,如果你只需要第一个维度大小能够变动,你可以 new 一个阵列的阵列(而不 
是阵列指标的阵列 "an array of pointers to arrays"): 
 
        const unsigned ncols = 100; 
        //'ncols' 不是执行期才决定的变数 (用来代表阵列的行数) 
 
        class Fred { ... }; 
 
        void manipulateArray(unsigned nrows) 
        //'nrows' 是执行期才决定的变数 (用来代表阵列的列数) 
        { 
          Fred (*matrix)[ncols] = new Fred[nrows][ncols]; 
 
          //用 matrix[i][j] 来处理 
 
          //deletion 是物件配置的逆运算: 
          delete [] matrix; 
        } 
 
如果你不光是需要在执行期改变阵列的第一个维度的话,就不能这样做了。 
 
======================================== 
 
Q42:怎样确保某类别的物件都是用 "new" 建立的,而非区域或整体/静态变数? 
 
确定该类别的建构子都是 "private:" 的,并定义个 "friend" 或 "static" 函数, 
来传回一个指向由 "new" 建造出来的物件(把建构子设成 "protected:",如果你想 
要有衍生类别的话)。 
 
        class Fred {    //只允许 Fred 动态地配置出来 
        public: 
          static Fred* create()                 { return new Fred();     } 
          static Fred* create(int i)            { return new Fred(i);    } 
          static Fred* create(const Fred& fred) { return new Fred(fred); } 
        private: 
          Fred(); 
          Fred(int i); 
          Fred(const Fred& fred); 
          virtual ~Fred(); 
        }; 
 
        main() 
        { 
          Fred* p = Fred::create(5); 
          ... 
          delete p; 
        } 
 
 
=============================== 
■□ 第10节:除错与错误处理 
=============================== 
 
Q43:怎样处理建构子的错误? 
 
丢出一个例外(throw an exception)。 
 
建构子没有传回值,所以不可能采用它传回的错误码。因此,侦测建构子错误最好的 
方法,就是丢出一个例外。 
 
在 C++ 编译器尚未提供例外处理之前,我们可先把物件置於「半熟」的状态(譬如 
:设个内部的状态位元),用个查询子("inspector")来检查该位元,就可让用户 
查看该物件是否还活著。也可以用另一个成员函数来检查该位元,若该物件没存活 
下来,就做个「没动作」(或是更狠的像是 "abort()" )的程式。但这实在很丑陋。 
 
======================================== 
 
Q44:如果建构子会丢出例外的话,该怎麽处理它的资源? 
 
物件里面的每个资料成员,都该自己收拾残局。 
 
如果建构子丢出一个例外的话,该物件的解构子就“不会”执行。如果你的物件得回 
复些曾做过的事情(像是配置记忆体、开启档案、锁定 semaphore),该物件内的资 
料成员就“必须”记住这个「必须恢复的东西」。 
 
举例来说:不要单单的把配置到的记忆体放入 "Fred*" 资料成员,而要放入一个「 
聪明的指标」(smart pointer) 资料成员中;当该“聪明指标”死掉的话,它的解构 
子就会删去 Fred 物件。 
 
【译注】「聪明的指标」(smart pointer) 在 Q4 中有提到一点。 
 
 
============================= 
■□ 第11节:Const 正确性 
============================= 
 
Q45:什麽是 "const correctness"? 
 
好问题。 
 
「常数正确性」乃使用 "const" 关键字,以确保常数物件不会被更动到。譬如:若 
"f()" 函数接收一个 "String",且 "f()" 想确保 "String" 不会被改变,你可以: 
 
 * 传值呼叫 (pass by value):    void  f(      String  s   )  { /*...*/ } 
 * 透过常数参考 (reference):    void  f(const String& s   )  { /*...*/ } 
 * 透过常数指标 (pointer)  :    void  f(const String* sptr)  { /*...*/ } 
 * 但不能用非常数参考      :    void  f(      String& s   )  { /*...*/ } 
 * 也不能用非常数指标      :    void  f(      String* sptr)  { /*...*/ } 
 
在接收 "const String&" 参数的函数里面,想更动到 "s" 的话,会产生个编译期的 
错误;没有牺牲任何执行期的空间及速度。 
 
宣告 "const" 参数也是另一种型别安全方法,就像一个常数字串,它会“丧失”各 
种可能会变更其内容的行为动作。如果你发现型别安全性质让你的系统正确地运作 
(这是真的;特别是大型的系统),你会发现「常数正确性」亦如是。 
 
======================================== 
 
Q46:我该早一点还是晚一点让东西有常数正确性? 
 
越越越早越好。 
 
延後补以常数正确性,会导致雪球效应:每次你在「这儿」用了 "const",你就得在 
「那儿」加上四个以上的 "const"。 
 
======================================== 
 
Q47:什麽是「const 成员函数」? 
 
一个只检测(而不更动)其物件的成员函数。 
 
        class Fred { 
        public: 
          void f() const; 
        };      // ^^^^^--- 暗示说 "fred.f()" 不会改变到 "fred" 
 
此乃意指:「抽象层次」的(用户可见的)物件状态不被改变(而不是许诺:该物件 
的「每一个位元内容」都不会被动到)。C++ 编译器不会对你许诺「每一个位元」这 
种事情,因为不是常数的别名(alias)就可能会修改物件的状态(把 "const" 指标 
黏上某个物件,并不能担保该物件不被改变;它只能担保该物件不会「被该指标的动 
作」所改变)。 
 
【译注】请逐字细读上面这句话。 
 
"const" 成员函数常被称作「查询子」(inspector),不是 "const" 的成员函数则 
称为「更动子」(mutator)。 
 
======================================== 
 
Q48:若我想在 "const" 成员函数内更新一个「看不见的」资料成员,该怎麽做? 
 
使用 "mutable" 或是 "const_cast"。 
【译注】这是很新的 ANSI C++ RTTI (RunTime Type Information) 规定,Borland 
        C++ 4.0 就率先提供了 const_cast 运算子。 
 
少数的查询子需要对资料成员做些无害的改变(譬如:"Set" 物件可能想快取它上一 
回所查到的东西,以加速下一次的查询)。此改变「无害」是指:此改变不会由物件 
的外部介面察觉出来(否则,该运作行为就该叫做更动子,而非查询子了)。 
 
这类情况下,会被更动的资料成员就该被标示成 "mutable"(把 "mutable" 关键字 
放在该资料成员宣告处前面;也就是和你放 "const" 一样的地方),这会告诉编译 
器:此资料成员允许 const 成员函数改变之。若你不能用 "mutable" 的话,可以用 
"const_cast" 把 "this" 的「常数性」给转型掉。譬如,在 "Set::lookup() const" 
里,你可以说: 
 
        Set* self = const_cast(this); 
 
这行执行之後,"self" 的位元内容就和 "this" 一样(譬如:"self==this"),但 
是 "self" 是一个 "Set*" 而非 "const Set*" 了,所以你就可以用 "self" 去修改 
"this" 指标所指向的物件。 
 
======================================== 
 
Q49:"const_cast" 会不会丧失最佳化的可能? 
 
理论上,是;实际上,否。 
 
就算编译器没真正做好 "const_cast",欲避免 "const" 成员函数被呼叫时,会造成 
暂存器快取区被清空的唯一方法,乃确保没有任何「非常数」的指标指向该物件。这 
种情况很难得会发生(当物件在 const 成员函数被启用的□围内被建立出来;当所 
有非 const 的成员函数在物件建立间启用,和 const 成员函数的启用被静态系结住 
;当所有的启用也都是 "inline";当建构子本身就是 "inline";和当建构子所呼叫 
的任何成员函数都是 inline 时)。 
 
【译注】这一段话很难翻得好(好啦好啦!我功力不足... :- 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 所覆盖的虚拟函数(通常是纯虚拟函数)。要用成份来做的话,太难了。 
 
        class Wilma { 
        protected: 
          void fredCallsWilma() 
            { cout << "Wilma::fredCallsWilma()\n"; wilmaCallsFred(); } 
          virtual void wilmaCallsFred() = 0; 
        }; 
 
        class Fred : private Wilma { 
        public: 
          void barney() 
            { cout << "Fred::barney()\n"; Wilma::fredCallsWilma(); } 
        protected: 
          virtual void wilmaCallsFred() 
            { cout << "Fred::wilmaCallsFred()\n"; } 
        }; 
 
======================================== 
 
Q71:我应该用指标转型方法,把「私有」衍生类别转成它的基底吗? 
 
当然不该。 
 
以私有衍生类别的运作行为、夥伴来看,从它上溯到基底类别的关系为已知的,所以 
从 PrivatelyDer* 往上转换成 Base*(或是从 PrivatelyDer& 到 Base&)是安全的 
;强制转型是不需要也不鼓励的。 
 
然而用 PrivateDer 的人应该避免这种不安全的转换,因为此乃立足於 PrivateDer 
的 "private" 决定,这个决定很容易在日後不经察觉就改变了。 
 
======================================== 
 
Q72:保护继承 (protected inheritance) 和私有继承有何关连? 
 
相似处:两者都能覆盖掉私有/保护基底类别的虚拟函数,两者都不把衍生的类别视 
为“一种”基底类别。 
 
不相似处:保护继承可让衍生类别的衍生类别知道它的继承关系(把实行细节显现出 
来)。它有好处(允许保护继承类别的子类别,藉这项关系来使用保护基底类别), 
也有代价(保护继承的类别,无法既想改变这种关系,而又不破坏到进一步的衍生类 
别)。 
 
保护继承使用 ": protected" 这种语法: 
 
        class Car : protected Engine { 
          //... 
        }; 
 
======================================== 
 
Q73:"private" 和 "protected" 的存取规则是什麽? 
 
拿底下这些类别当例子: 
 
        class B                    { /*...*/ }; 
        class D_priv : private   B { /*...*/ }; 
        class D_prot : protected B { /*...*/ }; 
        class D_publ : public    B { /*...*/ }; 
        class UserClass            { B b; /*...*/ }; 
 
没有一个子类别能存取到 B 的 private 部份。 
在 D_priv 内,B 的 public 和 protected 部份都变成 "private"。 
在 D_prot 内,B 的 public 和 protected 部份都变成 "protected"。 
在 D_publ 内,B 的 public 部份还是 public,protected 还是 protected 
 (D_publ is-a-kind-of-a B) 。 
Class "UserClass" 只能存取 B 的 public 部份,也就是:把 UserClass 从 B 那 
儿封起来了。 
 
欲把 B 的 public 成员在 D_priv 或 D_prot 内也变成 public,只要在该成员的名 
字前面加上 "B::"。譬如:想让 "B::f(int,float)" 成员在 D_prot 内也是 public 
的话,照这样写: 
 
        class D_prot : protected B { 
        public: 
          B::f;    //注意:不是写成 "B::f(int,float)" 
        }; 
 
 
====================================== 
■□ 第13节:抽象化(abstraction) 
====================================== 
 
Q74:分离介面与实作是做什麽用的? 
 
介面是企业体最有价值的资源。设计介面会比只把一堆独立的类别拼凑起来来得耗时 
,尤其是:介面需要花费更高阶人力的时间。 
 
既然介面是如此重要,它就应该保护起来,以避免被资料结构等等实作细节之变更所 
影响。因此你应该将介面与实作分离开来。 
 
======================================== 
 
Q75:在 C++ 里,我该怎样分离介面与实作(像 Modula-2 那样)? 
 
用 ABC(见下一则 FAQ)。 
 
======================================== 
 
Q76:ABC ("abstract base class") 是什麽? 
 
在设计层面,ABC 对应到抽象的概念。如果你问机械师父说他修不修运输工具,他可 
能会猜你心中想的到底是“哪一种”运输工具,他可能不会修理太空梭、轮船、脚踏 
车、核子潜艇。问题在於:「运输工具」是个抽象的概念(譬如:你建不出一辆「运 
输工具」,除非你知道要建的是“哪一种”)。在 C++,运输工具类别可当成是一个 
ABC,而脚踏车、太空梭……等等都当做它的子类别(轮船“是一种”运输工具)。 
在真实世界的 OOP 中,ABC 观念到处都是。 
 
在程式语言层面,ABC 是有一个以上纯虚拟成员函数(pure virtual)的类别(详见 
下一则 FAQ),你无法替一个 ABC 建造出物件(案例)来。 
 
======================================== 
 
Q77:「纯虚拟」(pure virtual) 成员函数是什麽? 
 
ABC 的某种成员函数,你只能在衍生的类别中实作它。 
 
有些成员函数只存於观念中,没有任何实质的定义。譬如,假设我要你画个 Shape, 
它位於 (x,y),大小为 7。你会问我「我该画哪一种 shape?」(圆、方、六边…… 
都有不同的画法。)在 C++ 里,我们可以先标出有一个叫做 "draw()" 这样的运作 
行为,且规定它只能(逻辑上)在子类别中定义出来: 
 
        class Shape { 
        public: 
          virtual void draw() const = 0; 
          //...                     ^^^--- "= 0" 指:它是 "pure virtual" 
        }; 
 
此纯虚拟函数让 "Shape" 变成一个 ABC。若你愿意,你可以把 "= 0" 语法想成是: 
该程式码是位於 NULL 指标处。因此,"Shape" 提供一个服务项目,但它现在尚无法 
提供实质的程式码以实现之。这样会确保:任何由 Shape 衍生出的 [具体的] 类别 
之物件,“将会”有那个我们事先规定的成员函数,即使基底类别尚无足够的资讯去 
真正的“定义”它。 
 
【译注】此处「定义」、「宣告」二词要分辨清楚! 
 
======================================== 
 
Q78:怎样替整个类别阶层提供列印的功能? 
 
提供一个 friend operator<< 去呼叫 protected 的虚拟函数: 
 
        class Base { 
        public: 
          friend ostream& operator<< (ostream& o, const Base& b) 
            { b.print(o); return o; } 
          //... 
        protected: 
          virtual void print(ostream& o) const;  //或 "=0;" 若 "Base" 是个 ABC 
        }; 
 
        class Derived : public Base { 
        protected: 
          virtual void print(ostream& o) const; 
        }; 
 
这样子所有 Base 的子类别只须提供它们自己的 "print(ostream&) const" 成员函 
数即可(它们都共用 "<<" operator)。这种技巧让夥伴像是有了动态系结的能力。 
 
======================================== 
 
Q79:何时该把解构子弄成 virtual? 
 
当你可能经由基底的指标去 "delete" 掉衍生的类别时。 
 
虚拟函数把某物件所属之真正类别所附的程式码,而非该指标/参考本身之类别所附 
的程式给系结上去。 当你说 "delete basePtr",且它的基底有虚拟解构子的话,则 
真正会被呼叫到的解构子,就是 *basePtr 物件之型态所属的解构子,而不是该指标 
本身之型态所附的解构子。一般说来这的确是一件好事。 
 
让你方便起见,你唯一不必将某类别的解构子设为 virtual 的场合是:「该类别“ 
没有”任何虚拟函数」。因为加入第一个虚拟函数,就会替每个物件都添加额外的空 
间负担(通常是一个机器 word 的大小),这正是编译器实作出动态系结的□密;它 
通常会替每个物件加入额外的指标,称为「虚拟指标表格」(virtual table pointer) 
,或是 "vptr" 。 
 
======================================== 
 
Q80:虚拟建构子 (virtual constructor) 是什麽? 
 
一种让你能做些 C++ 不直接支援的事情之惯用法。 
 
欲做出虚拟建构子的效果,可用个虚拟的 "createCopy()" 成员函数(用来做为拷贝 
建构子),或是虚拟的 "createSimilar()" 成员函数(用来做为预设建构子)。 
 
        class Shape { 
        public: 
          virtual ~Shape() { }          //详见 "virtual destructors" 
          virtual void draw() = 0; 
          virtual void move() = 0; 
          //... 
          virtual Shape* createCopy() const = 0; 
          virtual Shape* createSimilar() const = 0; 
        }; 
 
        class Circle : public Shape { 
        public: 
          Circle* createCopy()    const { return new Circle(*this); } 
          Circle* createSimilar() const { return new Circle(); } 
          //... 
        }; 
 
执行了 "Circle(*this)" 也就是执行了拷贝建构的行为(在这些运作行为中, 
"*this" 的型态为 "const Circle&")。"createSimilar()" 亦类似,但它乃建构出 
一个“预设的”Circle。 
 
这样用的话,就如同有了「虚拟建构子」(virtual constructors): 
 
        void userCode(Shape& s) 
        { 
          Shape* s2 = s.createCopy(); 
          Shape* s3 = s.createSimilar(); 
          //... 
          delete s2;    // 该解构子必须是 virtual 才行!! 
          delete s3;    // 如上. 
        } 
 
不论该 Shape 是 Circle、Square,甚或其他还不存在的 Shape 种类,这函数都能 
正确执行。 
】【http://www.trainlinux.com】【Close
『相关资料』
C++语言常见问题解答(3) (2002-01-23 14:27)
C++语言常见问题解答(4) (2002-01-23 14:27)
Linux下C语言编程--基础知识 (2002-01-22 14:26)
Linux下C语言编程--进程的创建 (2002-01-22 14:25)
Home 

诚恩Linux培训工作室