分享
 
 
 

C++语言常见问题解答(2)

王朝c/c++·作者佚名  2006-01-08
窄屏简体版  字體: |||超大  

==Part2/4============================

=============================

■□第9节:自由记忆体管理

=============================

Q33:"deletep"会删去"p"指标,还是它指到的资料,"*p"?

该指标指到的资料。

"delete"真正的意思是:「删去指标所指到的东西」(deletethethingpointed

toby)。同样的英文误用也发生在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))"不同,"newFred()"还会去呼叫

Fred的建构子。同理,"deletep"会去呼叫"*p"的解构子。

型别安全性:malloc()会传回一个不具型别安全的"void*",而"newFred()"则

会传回正确型态的指标(一个"Fred*")。

可被覆盖:"new"是个可被物件类别覆盖的运算子,而"malloc"不是以「各个类别

」作为覆盖的基准。

========================================

Q36:为什麽C++不替"new"及"delete"搭配个"realloc()"?

避免你产生意外。

当realloc()要拷贝配置区时,它做的是「逐位元bitwise」的拷贝,这会弄坏大

部份的C++物件。不过C++的物件应该要能自我拷贝才对:用它们自己的拷贝建构

子或设定运算子。

========================================

Q37:我该怎样配置/释放阵列?

用new[]和delete[]:

Fred*p=newFred[100];

//...

delete[]p;

每当你在"new"运算式中用了"[...]",你就必须在"delete"陈述中使用"[]"。

^^^^

这语法是必要的,因为「指向单一元素的指标」与「指向一个阵列的指标」在语法上

并无法区分开来。

========================================

Q38:万一我忘了将"[]"用在"delete"由"newFred[n]"配置到的阵列,会发生

什麽事?

灾难。

这是程式者的--而不是编译器的--责任,去确保new[]与delete[]的正确配

对。若你弄错了,编译器不会产生任何编译期或执行期的错误讯息。堆积(heap)被

破坏是最可能的结局,或是更糟的,你的程式会当掉。

========================================

Q39:成员函数做"deletethis"的动作是合法的(并且是好的)吗?

只要你小心的话就没事。

我所谓的「小心」是:

1)你得100%确定"this"是由"new"配置来的(而非"new[]",亦非自订的

"new"版本,一定要是最原始的"new")。

2)你得100%确定该成员函数是此物件最後一个会呼叫到的。

3)做完自杀的动作("deletethis;")後,你不能再去碰"this"的物件了,包

括资料及运作行为在内。

4)做完自杀的动作("deletethis;")後,你不能再去碰"this"指标了。

换句话说,你不能查看它、将它与其他指标或是NULL相比较、印出其值、

对它转型、对它做任何事情。

很自然的,这项警告也适用於:当"this"是个指向基底类别的指标,而解构子不是

virtual的场合。

========================================

Q40:我该怎麽用new来配置多维阵列?

有很多方法,端视你对阵列大小的伸缩性之要求而定。极端一点的情形,如果你在编

译期就知道所有阵列的维度,你可以静态地配置(就像C一样):

classFred{/*...*/};

voidmanipulateArray()

{

Fredmatrix[10][20];

//使用matrix[i][j]...

//不须特地去释放该阵列

}

另一个极端情况,如果你希望该矩阵的每个小块都能不一样大,你可以在自由记忆体

里配置之:

voidmanipulateArray(unsignednrows,unsignedncols[])

//'nrows'是该阵列之列数。

//所以合法的列数为(0,nrows-1)开区间。

//'ncols[r]'则是'r'列的行数('r'值域为[0..nrows-1])。

{

Fred**matrix=newFred*[nrows];

for(unsignedr=0;r<nrows;++r)

matrix[r]=newFred[ncols[r]];

//使用matrix[i][j]...

//释放就是配置的反动作:

for(r=nrows;r>0;--r)

delete[]matrix[r-1];

delete[]matrix;

}

========================================

Q41:C++能不能做到在执行时期才指定阵列的长度?

可以。STL有一个vectortemplate提供这种行为。请参考“程式库”一节的STL

项目。

不行。内建的阵列型态必须在编译期就指定它的长度了。

可以,内建的阵列可以在执行期才指定第一个索引的□围。譬如说,和上一则FAQ

相较,如果你只需要第一个维度大小能够变动,你可以new一个阵列的阵列(而不

是阵列指标的阵列"anarrayofpointerstoarrays"):

constunsignedncols=100;

//'ncols'不是执行期才决定的变数(用来代表阵列的行数)

classFred{...};

voidmanipulateArray(unsignednrows)

//'nrows'是执行期才决定的变数(用来代表阵列的列数)

{

Fred(*matrix)[ncols]=newFred[nrows][ncols];

//用matrix[i][j]来处理

//deletion是物件配置的逆运算:

delete[]matrix;

}

如果你不光是需要在执行期改变阵列的第一个维度的话,就不能这样做了。

========================================

Q42:怎样确保某类别的物件都是用"new"建立的,而非区域或整体/静态变数?

确定该类别的建构子都是"private:"的,并定义个"friend"或"static"函数,

来传回一个指向由"new"建造出来的物件(把建构子设成"protected:",如果你想

要有衍生类别的话)。

classFred{//只允许Fred动态地配置出来

public:

staticFred*create(){returnnewFred();}

staticFred*create(inti){returnnewFred(i);}

staticFred*create(constFred&fred){returnnewFred(fred);}

private:

Fred();

Fred(inti);

Fred(constFred&fred);

virtual~Fred();

};

main()

{

Fred*p=Fred::create(5);

...

deletep;

}

===============================

■□第10节:除错与错误处理

===============================

Q43:怎样处理建构子的错误?

丢出一个例外(throwanexception)。

建构子没有传回值,所以不可能采用它传回的错误码。因此,侦测建构子错误最好的

方法,就是丢出一个例外。

在C++编译器尚未提供例外处理之前,我们可先把物件置於「半熟」的状态(譬如

:设个内部的状态位元),用个查询子("inspector")来检查该位元,就可让用户

查看该物件是否还活著。也可以用另一个成员函数来检查该位元,若该物件没存活

下来,就做个「没动作」(或是更狠的像是"abort()")的程式。但这实在很丑陋。

========================================

Q44:如果建构子会丢出例外的话,该怎麽处理它的资源?

物件里面的每个资料成员,都该自己收拾残局。

如果建构子丢出一个例外的话,该物件的解构子就“不会”执行。如果你的物件得回

复些曾做过的事情(像是配置记忆体、开启档案、锁定semaphore),该物件内的资

料成员就“必须”记住这个「必须恢复的东西」。

举例来说:不要单单的把配置到的记忆体放入"Fred*"资料成员,而要放入一个「

聪明的指标」(smartpointer)资料成员中;当该“聪明指标”死掉的话,它的解构

子就会删去Fred物件。

【译注】「聪明的指标」(smartpointer)在Q4中有提到一点。

=============================

■□第11节:Const正确性

=============================

Q45:什麽是"constcorrectness"?

好问题。

「常数正确性」乃使用"const"关键字,以确保常数物件不会被更动到。譬如:若

"f()"函数接收一个"String",且"f()"想确保"String"不会被改变,你可以:

*传值呼叫(passbyvalue):voidf(Strings){/*...*/}

*透过常数参考(reference):voidf(constString&s){/*...*/}

*透过常数指标(pointer):voidf(constString*sptr){/*...*/}

*但不能用非常数参考:voidf(String&s){/*...*/}

*也不能用非常数指标:voidf(String*sptr){/*...*/}

在接收"constString&"参数的函数里面,想更动到"s"的话,会产生个编译期的

错误;没有牺牲任何执行期的空间及速度。

宣告"const"参数也是另一种型别安全方法,就像一个常数字串,它会“丧失”各

种可能会变更其内容的行为动作。如果你发现型别安全性质让你的系统正确地运作

(这是真的;特别是大型的系统),你会发现「常数正确性」亦如是。

========================================

Q46:我该早一点还是晚一点让东西有常数正确性?

越越越早越好。

延後补以常数正确性,会导致雪球效应:每次你在「这儿」用了"const",你就得在

「那儿」加上四个以上的"const"。

========================================

Q47:什麽是「const成员函数」?

一个只检测(而不更动)其物件的成员函数。

classFred{

public:

voidf()const;

};//^^^^^---暗示说"fred.f()"不会改变到"fred"

此乃意指:「抽象层次」的(用户可见的)物件状态不被改变(而不是许诺:该物件

的「每一个位元内容」都不会被动到)。C++编译器不会对你许诺「每一个位元」这

种事情,因为不是常数的别名(alias)就可能会修改物件的状态(把"const"指标

黏上某个物件,并不能担保该物件不被改变;它只能担保该物件不会「被该指标的动

作」所改变)。

【译注】请逐字细读上面这句话。

"const"成员函数常被称作「查询子」(inspector),不是"const"的成员函数则

称为「更动子」(mutator)。

========================================

Q48:若我想在"const"成员函数内更新一个「看不见的」资料成员,该怎麽做?

使用"mutable"或是"const_cast"。

【译注】这是很新的ANSIC++RTTI(RunTimeTypeInformation)规定,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*"而非"constSet*"了,所以你就可以用"self"去修改

"this"指标所指向的物件。

========================================

Q49:"const_cast"会不会丧失最佳化的可能?

理论上,是;实际上,否。

就算编译器没真正做好"const_cast",欲避免"const"成员函数被呼叫时,会造成

暂存器快取区被清空的唯一方法,乃确保没有任何「非常数」的指标指向该物件。这

种情况很难得会发生(当物件在const成员函数被启用的□围内被建立出来;当所

有非const的成员函数在物件建立间启用,和const成员函数的启用被静态系结住

;当所有的启用也都是"inline";当建构子本身就是"inline";和当建构子所呼叫

的任何成员函数都是inline时)。

【译注】这一段话很难翻得好(好啦好啦!我功力不足...:-<),所以附上原文:

Evenifacompileroutlawed"const_cast",theonlywaytoavoidflushing

theregistercacheacrossa"const"memberfunctioncallwouldbeto

ensurethattherearenonon-constpointersthataliastheobject.This

canonlyhappeninrarecases(whentheobjectisconstructedinthescope

oftheconstmemberfninvocation,andwhenallthenon-constmember

functioninvocationsbetweentheobject'sconstructionandtheconst

memberfninvocationarestaticallybound,andwheneveryoneofthese

invocationsisalso"inline"d,andwhentheconstructoritselfis"inline"d,

andwhenanymemberfnstheconstructorcallsareinline).

=====================

■□第12节:继承

=====================

Q50:「继承」对C++来说很重要吗?

是的。

「继承」是抽象化资料型态(abstractdatatype,ADT)与OOP的一大分野。

========================================

Q51:何时该用继承?

做为一个「特异化」(specialization)的机制。

人类以两种角度来抽象化事物:「部份」(part-of)和「种类」(kind-of)。福特汽

车“是一种”(is-a-kind-of-a)车子,福特汽车“有”(has-a)引擎、轮胎……等

等零件。「部份」的层次随著ADT的流行,已成为软体系统的一份子了;而「继承

」则添入了“另一个”重要的软体分解角度。

========================================

Q52:怎样在C++中表现出继承?

用":public"语法:

classCar:publicVehicle{

//^^^^^^^^----":public"读作「是一种」("is-a-kind-of-a")

//...

};

我们以几种方式来描述上面的关系:

*Car是「一种」("akindofa")Vehicle

*Car乃「衍生自」("derivedfrom")Vehicle

*Car是个「特异化的」("aspecialized")Vehicle

*Car是Vehicle的「子类别」("subclass")

*Vehicle是Car的「基底类别」("baseclass")

*Vehicle是Car的「父类别」("superclass")(这不是C++界常用的说法)

【译注】"superclass"是Smalltalk语言的关键字。

========================================

Q53:把衍生类别的指标转型成指向它的基底,可以吗?

可以。

衍生类别是该基底类别的特异化版本(衍生者「是一种」("a-kind-of")基底)。这

种向上的转换是绝对安全的,而且常常会发生(如果我指向一个汽车Car,实际上我

是指向一个车子Vehicle):

voidf(Vehicle*v);

voidg(Car*c){f(c);}//绝对很安全;不需要转型

注意:在这里我们假设的是"public"的继承;後面会再提到「另一种」"private/

protected"的继承。

========================================

Q54:Derived*-->Base*是正常的;那为什麽Derived**-->Base**则否?

C++让Derived*能转型到Base*,是因为衍生的物件「是一种」基底的物件。然而

想由Derived**转型到Base**则是错误的!要是能够的话,Base**就可能会被解

参用(产生一个Base*),该Base*就可能指向另一个“不一样的”衍生类别,这

是不对的。

照此看来,衍生类别的阵列就「不是一种」基底类别的阵列。在ParadigmShift公

司的C++训练课程里,我们用底下的例子来比喻:

"一袋苹果「不是」一袋水果".

"AbagofapplesisNOTabagoffruit".

如果一袋苹果可以当成一袋水果来传递,别人就可能把香蕉放到苹果袋里头去!

========================================

Q55:衍生类别的阵列「不是」基底的阵列,是否表示阵列不好?

没错,「阵列很烂」(开玩笑的:-)。

C++内建的阵列有一个不易察觉的问题。想一想:

voidf(Base*arrayOfBase)

{

arrayOfBase[3].memberfn();

}

main()

{

DerivedarrayOfDerived[10];

f(arrayOfDerived);

}

编译器认为这完全是型别安全的,因为由Derived*转换到Base*是正常的。但事

实上这很差劲:因为Derived可能会比Base还要大,f()里头的阵列索引不光是

没有型别安全,甚至还可能没指到真正的物件呢!通常它会指到某个倒楣的

Derived物件的中间去。

根本的问题在於:C++不能分辨出「指向一个东西」和「指向一个阵列」。很自然的

,这是C++“继承”自C语言的特徵。

注意:如果我们用的是一个像阵列的「类别」而非最原始的阵列(譬如:"Array"

而非"T[]"),这问题就可以在编译期被挑出来,而非在执行的时候。

==========================

●12A:继承--虚拟函数

==========================

Q56:什麽是「虚拟成员函数」?

虚拟函数可让衍生的类别「取代」原基底类别所提供的运作。只要某物件是衍生出来

的,就算我们是透过基底物件的指标,而不是以衍生物件的指标来存取该物件,编译

器仍会确保「取代後」的成员函数被呼叫。这可让基底类别的演算法被衍生者所替换

,即使我们不知道衍生类别长什麽样子。

注意:衍生的类别亦可“部份”取代(覆盖,override)掉基底的运作行为(如有必

要,衍生类别的运作行为亦可呼叫它的基底类别版本)。

========================================

Q57:C++怎样同时做到动态系结和静态型别?

底下的讨论中,"ptr"指的是「指标」或「参考」。

一个ptr有两种型态:静态的ptr型态,与动态的「被指向的物件」的型态(该物

件可能实际上是个由其他类别衍生出来的类别的ptr)。

「静态型别」("statictyping")是指:该呼叫的「合法性」,是以ptr的静态型

别为侦测之依据,如果ptr的型别能处理成员函数,则「指向的物件」自然也能。

「动态系结」("dynamicbinding")是指:「程式码」呼叫是以「被指向的物件」之

型态为依据。被称为「动态系结」,是因为真正会被呼叫的程式码是动态地(於执行

时期)决定的。

========================================

Q58:衍生类别能否将基底类别的非虚拟函数覆盖(override)过去?

可以,但不好。

C++的老手有时会重新定义非虚拟的函数,以提升效率(换一种可能会运用到衍生类

别才有的资源的作法),或是用以避开遮蔽效应(hidingrule,底下会提,或是看

看ARM["AnnotatedReferenceManual"]sect.13.1),但是用户的可见性效果必

须完全相同,因为非虚拟的函数是以指标/参考的静态型别为分派(dispatch)的依

据,而非以指到的/被参考到的物件之动态型别来决定。

========================================

Q59:"Warning:Derived::f(int)hidesBase::f(float)"是什麽意思?

这是指:你死不了的。

你出的问题是:如果Derived宣告了个叫做"f"的成员函数,Base却早已宣告了

个不同型态签名型式(譬如:参数型态或是const不同)的"f",这样子Base"f"

就会被「遮蔽hide」住,而不是被「多载overload」或「覆盖override」(即使

Base"f"已经是虚拟的了)。

解决法:Derived要替Base被遮蔽的成员函数重新定义(就算它不是虚拟的)。通

常重定义的函数,仅仅是去呼叫合适的Base成员函数,譬如:

classBase{

public:

voidf(int);

};

classDerived:publicBase{

public:

voidf(double);

voidf(inti){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:该怎麽表达出「私有继承」(privateinheritance)?

用":private"来代替":public."譬如:

classFoo:privateBar{

//...

};

================================

Q69:「私有继承」和「成份」(composition)有多类似?

私有继承是「成份」(has-a)的一种语法变形。

譬如:「汽车有引擎」("carhas-aengine")关系可用成份来表达:

classEngine{

public:

Engine(intnumCylinders);

voidstart();//startsthisEngine

};

classCar{

public:

Car():e_(8){}//initializesthisCarwith8cylinders

voidstart(){e_.start();}//startthisCarbystartingitsengine

private:

Enginee_;

};

同样的"has-a"关系也可用私有继承来表达:

classCar:privateEngine{

public:

Car():Engine(8){}//initializesthisCarwith8cylinders

Engine::start;//startthisCarbystartingitsengine

};

这两种型式的成份有几分相似性:

*这两种情况之下,Car只含有一个Engine成员物件。

*两种情况都不能让(外界)使用者由Car*转换成Engine*。

也有几个不同点:

*如果你想要让每个Car都含有数个Engine的话,就得用第一个型式。

*第二个型式可能会导致不必要的多重继承(multipleinheritance)。

*第二个型式允许Car的成员从Car*转换成Engine*。

*第二个型式可存取到基底类别的"protected"成员。

*第二个型式允许Car覆盖掉Engine的虚拟函数。

注意:私有继承通常是用来获得基底类别"protected:"成员的存取权力,但这通常

只是个短程的解决方案。

========================================

Q70:我比较该用哪一种:成份还是私有继承?

成份。

正常情形下,你不希望存取到太多其他类别的内部,但私有继承会给你这些额外的权

力(与责任)。不过私有继承不是洪水猛兽;它只是得多花心力去维护罢了,因为它

增加了别人动到你的东西、让你的程式出差错的机会。

合法而长程地使用私有继承的时机是:当你想新建一个Fred类别,它会用到Wilma

类别的程式码,而且Wilma的程式码也会呼叫到你这个Fred类别里的运作行为时

。这种情形之下,Fred呼叫了Wilma的非虚拟函数,Wilma也呼叫了它自己的、会

被Fred所覆盖的虚拟函数(通常是纯虚拟函数)。要用成份来做的话,太难了。

classWilma{

protected:

voidfredCallsWilma()

{cout<<"Wilma::fredCallsWilma()\n";wilmaCallsFred();}

virtualvoidwilmaCallsFred()=0;

};

classFred:privateWilma{

public:

voidbarney()

{cout<<"Fred::barney()\n";Wilma::fredCallsWilma();}

protected:

virtualvoidwilmaCallsFred()

{cout<<"Fred::wilmaCallsFred()\n";}

};

========================================

Q71:我应该用指标转型方法,把「私有」衍生类别转成它的基底吗?

当然不该。

以私有衍生类别的运作行为、夥伴来看,从它上溯到基底类别的关系为已知的,所以

从PrivatelyDer*往上转换成Base*(或是从PrivatelyDer&到Base&)是安全的

;强制转型是不需要也不鼓励的。

然而用PrivateDer的人应该避免这种不安全的转换,因为此乃立足於PrivateDer

的"private"决定,这个决定很容易在日後不经察觉就改变了。

========================================

Q72:保护继承(protectedinheritance)和私有继承有何关连?

相似处:两者都能覆盖掉私有/保护基底类别的虚拟函数,两者都不把衍生的类别视

为“一种”基底类别。

不相似处:保护继承可让衍生类别的衍生类别知道它的继承关系(把实行细节显现出

来)。它有好处(允许保护继承类别的子类别,藉这项关系来使用保护基底类别),

也有代价(保护继承的类别,无法既想改变这种关系,而又不破坏到进一步的衍生类

别)。

保护继承使用":protected"这种语法:

classCar:protectedEngine{

//...

};

========================================

Q73:"private"和"protected"的存取规则是什麽?

拿底下这些类别当例子:

classB{/*...*/};

classD_priv:privateB{/*...*/};

classD_prot:protectedB{/*...*/};

classD_publ:publicB{/*...*/};

classUserClass{Bb;/*...*/};

没有一个子类别能存取到B的private部份。

在D_priv内,B的public和protected部份都变成"private"。

在D_prot内,B的public和protected部份都变成"protected"。

在D_publ内,B的public部份还是public,protected还是protected

(D_publis-a-kind-of-aB)。

Class"UserClass"只能存取B的public部份,也就是:把UserClass从B那

儿封起来了。

欲把B的public成员在D_priv或D_prot内也变成public,只要在该成员的名

字前面加上"B::"。譬如:想让"B::f(int,float)"成员在D_prot内也是public

的话,照这样写:

classD_prot:protectedB{

public:

B::f;//注意:不是写成"B::f(int,float)"

};

======================================

■□第13节:抽象化(abstraction)

======================================

Q74:分离介面与实作是做什麽用的?

介面是企业体最有价值的资源。设计介面会比只把一堆独立的类别拼凑起来来得耗时

,尤其是:介面需要花费更高阶人力的时间。

既然介面是如此重要,它就应该保护起来,以避免被资料结构等等实作细节之变更所

影响。因此你应该将介面与实作分离开来。

========================================

Q75:在C++里,我该怎样分离介面与实作(像Modula-2那样)?

用ABC(见下一则FAQ)。

========================================

Q76:ABC("abstractbaseclass")是什麽?

在设计层面,ABC对应到抽象的概念。如果你问机械师父说他修不修运输工具,他可

能会猜你心中想的到底是“哪一种”运输工具,他可能不会修理太空梭、轮船、脚踏

车、核子潜艇。问题在於:「运输工具」是个抽象的概念(譬如:你建不出一辆「运

输工具」,除非你知道要建的是“哪一种”)。在C++,运输工具类别可当成是一个

ABC,而脚踏车、太空梭……等等都当做它的子类别(轮船“是一种”运输工具)。

在真实世界的OOP中,ABC观念到处都是。

在程式语言层面,ABC是有一个以上纯虚拟成员函数(purevirtual)的类别(详见

下一则FAQ),你无法替一个ABC建造出物件(案例)来。

========================================

Q77:「纯虚拟」(purevirtual)成员函数是什麽?

ABC的某种成员函数,你只能在衍生的类别中实作它。

有些成员函数只存於观念中,没有任何实质的定义。譬如,假设我要你画个Shape,

它位於(x,y),大小为7。你会问我「我该画哪一种shape?」(圆、方、六边……

都有不同的画法。)在C++里,我们可以先标出有一个叫做"draw()"这样的运作

行为,且规定它只能(逻辑上)在子类别中定义出来:

classShape{

public:

virtualvoiddraw()const=0;

//...^^^---"=0"指:它是"purevirtual"

};

此纯虚拟函数让"Shape"变成一个ABC。若你愿意,你可以把"=0"语法想成是:

该程式码是位於NULL指标处。因此,"Shape"提供一个服务项目,但它现在尚无法

提供实质的程式码以实现之。这样会确保:任何由Shape衍生出的[具体的]类别

之物件,“将会”有那个我们事先规定的成员函数,即使基底类别尚无足够的资讯去

真正的“定义”它。

【译注】此处「定义」、「宣告」二词要分辨清楚!

========================================

Q78:怎样替整个类别阶层提供列印的功能?

提供一个friendoperator<<去呼叫protected的虚拟函数:

classBase{

public:

friendostream&operator<<(ostream&o,constBase&b)

{b.print(o);returno;}

//...

protected:

virtualvoidprint(ostream&o)const;//或"=0;"若"Base"是个ABC

};

classDerived:publicBase{

protected:

virtualvoidprint(ostream&o)const;

};

这样子所有Base的子类别只须提供它们自己的"print(ostream&)const"成员函

数即可(它们都共用"<<"operator)。这种技巧让夥伴像是有了动态系结的能力。

========================================

Q79:何时该把解构子弄成virtual?

当你可能经由基底的指标去"delete"掉衍生的类别时。

虚拟函数把某物件所属之真正类别所附的程式码,而非该指标/参考本身之类别所附

的程式给系结上去。当你说"deletebasePtr",且它的基底有虚拟解构子的话,则

真正会被呼叫到的解构子,就是*basePtr物件之型态所属的解构子,而不是该指标

本身之型态所附的解构子。一般说来这的确是一件好事。

让你方便起见,你唯一不必将某类别的解构子设为virtual的场合是:「该类别“

没有”任何虚拟函数」。因为加入第一个虚拟函数,就会替每个物件都添加额外的空

间负担(通常是一个机器word的大小),这正是编译器实作出动态系结的□密;它

通常会替每个物件加入额外的指标,称为「虚拟指标表格」(virtualtablepointer)

,或是"vptr"。

========================================

Q80:虚拟建构子(virtualconstructor)是什麽?

一种让你能做些C++不直接支援的事情之惯用法。

欲做出虚拟建构子的效果,可用个虚拟的"createCopy()"成员函数(用来做为拷贝

建构子),或是虚拟的"createSimilar()"成员函数(用来做为预设建构子)。

classShape{

public:

virtual~Shape(){}//详见"virtualdestructors"

virtualvoiddraw()=0;

virtualvoidmove()=0;

//...

virtualShape*createCopy()const=0;

virtualShape*createSimilar()const=0;

};

classCircle:publicShape{

public:

Circle*createCopy()const{returnnewCircle(*this);}

Circle*createSimilar()const{returnnewCircle();}

//...

};

执行了"Circle(*this)"也就是执行了拷贝建构的行为(在这些运作行为中,

"*this"的型态为"constCircle&")。"createSimilar()"亦类似,但它乃建构出

一个“预设的”Circle。

这样用的话,就如同有了「虚拟建构子」(virtualconstructors):

voiduserCode(Shape&s)

{

Shape*s2=s.createCopy();

Shape*s3=s.createSimilar();

//...

deletes2;//该解构子必须是virtual才行!!

deletes3;//如上.

}

不论该Shape是Circle、Square,甚或其他还不存在的Shape种类,这函数都能

正确执行。

 
 
 
免责声明:本文为网络用户发布,其观点仅代表作者个人观点,与本站无关,本站仅提供信息存储服务。文中陈述内容未经本站证实,其真实性、完整性、及时性本站不作任何保证或承诺,请读者仅作参考,并请自行核实相关内容。
2023年上半年GDP全球前十五强
 百态   2023-10-24
美众议院议长启动对拜登的弹劾调查
 百态   2023-09-13
上海、济南、武汉等多地出现不明坠落物
 探索   2023-09-06
印度或要将国名改为“巴拉特”
 百态   2023-09-06
男子为女友送行,买票不登机被捕
 百态   2023-08-20
手机地震预警功能怎么开?
 干货   2023-08-06
女子4年卖2套房花700多万做美容:不但没变美脸,面部还出现变形
 百态   2023-08-04
住户一楼被水淹 还冲来8头猪
 百态   2023-07-31
女子体内爬出大量瓜子状活虫
 百态   2023-07-25
地球连续35年收到神秘规律性信号,网友:不要回答!
 探索   2023-07-21
全球镓价格本周大涨27%
 探索   2023-07-09
钱都流向了那些不缺钱的人,苦都留给了能吃苦的人
 探索   2023-07-02
倩女手游刀客魅者强控制(强混乱强眩晕强睡眠)和对应控制抗性的关系
 百态   2020-08-20
美国5月9日最新疫情:美国确诊人数突破131万
 百态   2020-05-09
荷兰政府宣布将集体辞职
 干货   2020-04-30
倩女幽魂手游师徒任务情义春秋猜成语答案逍遥观:鹏程万里
 干货   2019-11-12
倩女幽魂手游师徒任务情义春秋猜成语答案神机营:射石饮羽
 干货   2019-11-12
倩女幽魂手游师徒任务情义春秋猜成语答案昆仑山:拔刀相助
 干货   2019-11-12
倩女幽魂手游师徒任务情义春秋猜成语答案天工阁:鬼斧神工
 干货   2019-11-12
倩女幽魂手游师徒任务情义春秋猜成语答案丝路古道:单枪匹马
 干货   2019-11-12
倩女幽魂手游师徒任务情义春秋猜成语答案镇郊荒野:与虎谋皮
 干货   2019-11-12
倩女幽魂手游师徒任务情义春秋猜成语答案镇郊荒野:李代桃僵
 干货   2019-11-12
倩女幽魂手游师徒任务情义春秋猜成语答案镇郊荒野:指鹿为马
 干货   2019-11-12
倩女幽魂手游师徒任务情义春秋猜成语答案金陵:小鸟依人
 干货   2019-11-12
倩女幽魂手游师徒任务情义春秋猜成语答案金陵:千金买邻
 干货   2019-11-12
 
推荐阅读
 
 
 
>>返回首頁<<
 
靜靜地坐在廢墟上,四周的荒凉一望無際,忽然覺得,淒涼也很美
© 2005- 王朝網路 版權所有