分享
 
 
 

转贴:C++语言常见问题解:#54 ~ #80

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

这是我从台湾的http://www.cis.nctu.edu.tw/chinese/doc/research/c++/C++FAQ-Chinese/发现的《C++ Frequently Asked Questions》的繁体翻译,作者是:叶秉哲,也是《C++ Programming Language》3/e繁体版的译者,该文章是非常的好,出于学习用途而将它转贴,本人未取得作者的授权,原文章的版权仍然归属原作者.

C++语言常见问题解

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>"

而非 "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 种类,这函数都能

正确执行。

--

Marshall Cline

--

Marshall P. Cline, Ph.D. / Paradigm Shift Inc / PO Box 5108 / Potsdam NY 13676

cline@sun.soe.clarkson.edu / 315-353-6100 / FAX: 315-353-6110

 
 
 
免责声明:本文为网络用户发布,其观点仅代表作者个人观点,与本站无关,本站仅提供信息存储服务。文中陈述内容未经本站证实,其真实性、完整性、及时性本站不作任何保证或承诺,请读者仅作参考,并请自行核实相关内容。
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- 王朝網路 版權所有