(Effective C++)第六章 继承与面向对象(Inheritance and Object-Oriented Design)

Public继承意味着“is-a”,virtual函数意味着“必须被继承”,non-virtual意味着“接口和实现都必须被继承”。

8.1 条款32:确定你的public继承塑模出is-a关系 (Make sure public inheritance models “is-a”)

C++最重要的一个规则是:public inheritance(公开继承)意味“is-a”(是一种)的关系。如果你令class D(“Derived”)以public方式继承class B(“Base”),你便是告诉C++编译器说,每个类型为D的对象同时也是一个类型为B的对象,反之不成立。适用于base classes身上的每一件事情一定也适用于derived class身上。
但是有这样的一个例子,企鹅(penguin)是一种鸟,鸟可以飞,但是企鹅不能飞。我们以继承关系,它塑模出较佳的真实性,如下:
class Bird {
。。。   //没有声明fly函数
};
class FlyingBird:public Bird{
public:
virtual void fly();

};      
class Penguin : public Bird{ //没有声明fly函数

};
示例8-1-1  企鹅不会飞
此刻,企鹅是鸟,但是不能飞。
世界上并不存在一个“适用于所有软件”的完美设计。所谓最佳设计,取决于系统希望做什么事,包括现在和未来。

class Bird {//声明fly函数
public:
virtual void fly();
};
class Penguin : public Bird{
public:
virtual void fly() { error();}
};
示例8-1-2  企鹅尝试飞,是一种错误
另有一种实现派别是,企鹅可以尝试飞,但是那么做是一种错误。“企鹅不会飞”这个限制可由编译期间强制实施,但是“企鹅尝试飞行,是一种错误”这条规则,只有运行期间才能检测出来。
条款18说过,好的借口可以防止无效的代码通过编译,因此我们更偏重于“企鹅不会飞”的设计。
is-a并非是唯一存在于class之间的关系。另外两个常见的关系是has-a(有一个)和is-implementation-in-terms-of(根据某物实现出)。分别在条款38和39讨论。

8.2 条款33:避免遮掩继承而来的名称 (Avoid hiding inherited names)

C++的名称遮掩规则所作的唯一事情,就是遮掩名称,不管名称是否有相同的类型,用内层作用域的名称遮掩外层作用域的名称。Derived classes的名称会遮掩base classes的名称。
成员函数被重载的特征:
(1)相同的范围(在同一个类中);
(2)函数名字相同;
(3)参数不同;
(4)virtual关键字可有可无。
(5)返回值可以不同。
    覆盖是指派生类函数覆盖基类函数,特征是:
(1)不同的范围(分别位于派生类与基类);
(2)函数名字相同;
(3)参数相同;
(4)基类函数必须有virtual关键字。
“隐藏”是指派生类的函数屏蔽了与其同名的基类函数,规则如下:
(1)如果派生类的函数与基类的函数同名,但是参数不同。此时,不论有无virtual关键字,基类的函数将被隐藏(注意别与重载混淆)。
(2)如果派生类的函数与基类的函数同名,并且参数也相同,但是基类函数没有virtual关键字。此时,基类的函数被隐藏(注意别与覆盖混淆)。

下例是derived class作用域被嵌套在base class作用域内,如
class Base {
public:
virtual void mf1()=0;
virtual void mf2();
void mf3();

private:
int x;
};
class Derived:public Base{
public:
virtual void mf1();
void mf4();

};      
void Derived:: mf4{
     mf2();
};
示例8-2-1  名称被遮掩-
编译器的做法是查找local作用域(也就是mf4覆盖的作用域),是否有mf2;查找其外围作用域,也就是class Derived覆盖的作用域,继续往外围移动,本例是base class,查找内含Base class的那个namespaces的作用域,最后查global作用域。

class Base {
public:
virtual void mf1()=0;
virtual void mf1(int);
virtual void mf2();
void mf3();
virtual void mf3(double);

private:
int x;
};
class Derived:public Base{
public:
virtual void mf1();
void mf3();
void mf4();

};      
void Derived:: mf4{
     mf2();
};

//使用
Derived d;
int x;
d.mf1();  //ok,调用Derived::mf1
d.mf1(x); //error,因为Derived::mf1遮掩了Base::mf1
d.mf2();  //ok,调用Base::mf2
d.mf3();  //ok,调用Derived::mf3
d.mf3(x); //error,因为Derived::mf3遮掩了Base::mf3
示例8-2-2  名称被遮掩二
解决之道之一,使用using 声明式:
class Derived:public Base{
public:
using Base::mf1; //让Base class内名为mf1和mf3的所有东西
using Base::mf3; //在Derived class作用域可见
virtual void mf1();
void mf3();
void mf4();

};      

//使用
Derived d;
int x;
d.mf1();  //ok,调用Derived::mf1
d.mf1(x); //ok,因为调用Base::mf1
d.mf2();  //ok,调用Base::mf2
d.mf3();  //ok,调用Derived::mf3
d.mf3(x); //ok,因为调用Base::mf3
示例8-2-3  名称被遮掩三
解决之道之二,使用转交函数(forwarding function):
class Derived:private Base{
public:
virtual void mf1(){ //转交函数,暗自成为inline,见条款30
Base::mf1();
};

};      

//使用
Derived d;
int x;
d.mf1();  //ok,调用Derived::mf1
d.mf1(x); //error, 因为Derived::mf1遮掩了Base::mf1
示例8-2-4  名称被遮掩四
Inline转交函数的另一个用途是为那些不支持using声明式的老旧编译器另辟一条新路,将继承而得的名称汇入derived class作用域内。

8.3 条款34:区分借口继承和实现继承 (Differentiate between inheritance of interface and inheritance of implementation)

Public继承是由两部分组成:函数接口(function interfaces)继承和函数实现(function implementation)继承。
身为class设计者,有时候希望derived class只继承成员函数接口;有时候希望同时继承函数接口和实现;有时候希望覆写他们所继承的实现;有时候希望derived classes同时继承函数的接口和实现,但是不允许覆写任何东西。

class Shape {
public:
virtual void draw() const = 0;
virtual void error(const std::string &msg);
void objectID() const;

};
class Rectangle:public Shape {…};
class Ellipse:public Shape {…};

//使用
Shape *ps = new Shape; //error,Shape是抽象的
Shape *ps1 = new Rectangle;
ps1->draw();
shape ps2 = new Ellipse;
ps2->draw();
ps1->Shape::draw();
ps2->Shape::draw();
示例8-3-1  名称被遮掩四
?    成员函数的接口总是会被继承。
Shape class声明了三个函数,都被继承下来。

?    声明一个pure virtual函数的目的是为了让derived classes只继承函数接口。
你必须提供一个draw哈思楠,但是不干涉你怎么实现它。

?    声明简朴的非纯虚(impure virtual)函数的目的,是让derived classes继承该函数的接口和缺省实现。
避免将所有成员函数声明为virtual。某些函数就是不该在derived class中被重新定义。

?    声明non-virtual函数的目的是为了令derived classes继承函数的接口及一份强制性实现。
每个Shape对象都有一个用来产生对象识别码的函数,此识别码总是采用相同的计算方法,该方法用Shape::objected的定义式决定。
由于non-virtual函数代表的意义是不变性(invariant)凌驾特异性(specialization),所以它绝不会在derived class中被重新定义的。
避免将所有函数声明为non-virtual,否则,会使得derived classes没有余裕空间进行特化工作。

8.4 条款35:考虑virtual函数以外的其他选择 (consider alternatives to virtual functions)

假设你正在写一个视频游戏软件,你打算为游戏内的人物设计一个继承体系。你因此决定提供一个成员函数healthValue,它会返回一个整数,表示人物的健康程度。于是,将healthValue声明为virtual是再明白不过的做法(条款34):
class GameCharacter {
public:
virtual int healthValue() const; //返回人物的健康指数,
//derived classes可重新定义它
};
示例8-4-1  healthValue的虚函数声明
?    借由Non-Virtual Interface手法实现Template Method模式。
该流派的拥护建议是,较好的设计是保留healthvalue为public成员函数,但是让它成为non-virtual,并调用一个private函数进行实际的工作。

class GameCharacter {
public:
virtual int healthValue() const //derived class不重新定义它,条款36
{
    。。。//做一些事前工作,锁定互斥器,日志记录,验证class约束条件
    int ret = doHealthVaule();  //做真正的工作
    …  ///做一些事后工作,解除锁定,再次验证class约束条件
    return ret;
}
private:
virtual int doHealthValue const //derived classes可重新定义它
{
…      //缺省算法,计算健康指数
}
};
示例8-4-2  healthValue的NVI手法
这一基本设计,也就是“令客户通过public non-virtual成员函数间接调用private virtual函数”,称为non-virtual interface(NVI)手法。它是所谓Template Method设计模式(与C++templates并无关联)的一个独特表示形式。我把这个non-virtual函数(healthValue)称为virtual函数的外覆器(wrapper)。
在NVI手法下其实没有必要让virtual函数一定得是private。“重新定义virtual函数”表示某些事“如何被完成”,“调用virtual函数“则表示它”何时“被完成。这些事情都是独立不相干的。

?    借由Function Pointers实现Strategy模式。
另一个更戏剧性的设计主张是“人物健康指数的计算与人物类型无关“。例如,我们可能要求每个人物的构造函数接收一个指针,指向一个健康计算函数,而我们可以调用该函数进行实际计算。

class GameCharacter;   //前置声明
//计算健康指数的缺省算法
int defaultHealthCalc(const GameCharacter &gc);
class GameCharacter {
public:
typedef int (*HealthCalcFunc)(const GameCharacter &);
explicit GameCharacter(HealthCalcFunc hcf = defaultHealthCalc)
:healthFunc(hcf)
{}
int healthVaule() const
{ reurn healthFunc(*this);}
private:
HealthCalcFunc healthFunc;
};
示例8-4-3 healthValue的Function Pointers手法
这个做法是常见的Strategy设计模式的简单应用。它还提供了某些有趣弹性(优点):
1)同一人物类型之不同实体可以有不同的健康计算函数。例如
2)某已知人物之健康指数计算函数可在运行期变更。例如GameCharacter可提供一个成员函数setHealthCalculator,用来替换当前的健康指数计算函数。

这种设计,如果需要non-public信息进行精确计算,就需要public member成员函数提供相应的访问权限。实际上任何时候当你将class内的某个机能(也许取道某个成员函数)替换为class外部的某个等价机能(也许取道某个non-member,non-friend函数或另个class的non-friend成员函数),这都是潜在争议点。
一般而言,唯一能够解决“需要以non-member函数访问class的non-public成分“的办法就是:弱化class的封装。例如,class可声明那个non-member函数为friends。

?    借由trl::function实现Strategy模式。
如果我们不再使用函数指针,而是改用一个类型为trl::function的对象。如下

class GameCharacter;   //前置声明
//计算健康指数的缺省算法
int defaultHealthCalc(const GameCharacter &gc);
class GameCharacter {
public:
// HealthCalcFunc可以是任何可调用物,可被调用并接受任何兼容于
//GameCharacter植物,返回任何兼容于int的东西
typedef std::trl::function <int (const GameCharacter &)> HealthCalcFunc;
explicit GameCharacter(HealthCalcFunc hcf = defaultHealthCalc)
:healthFunc(hcf)
{}
int healthVaule() const
{ reurn healthFunc(*this);}
private:
HealthCalcFunc healthFunc;
};
示例8-4-4 healthValue的Function Pointers手法
如你所见,HealthCalcFunc是个typedef,用来表现trl::function的某个具现体,意味该具现体的行为像一般的函数指针。<int (const GameCharacter &)>的红色部分是具现体(instantiation)的目标签名式(target signature)。这个trl::function所产生的对象可以持有任何与此签名时兼容的可调用物(callable entity)。所谓兼容,意思就是这个可调用物的参数可被隐式转换为const GameCharacter &,而其返回类型可被隐式转为int。
和前一个设计相比,这个设计几乎相同,唯一不同的是GameCharacter持有一个trl::function对象,相当于一个指向函数的泛化指针,更具有弹性。
short calchealth(const GameCharater &); //健康计算函数,返回类型为non-int
struct healthcalculator{  //健康计算函数对象
  int operator()(const GameCharater &) const
{…}
};
class GameLevel{
public:
//成员函数,计算健康,返回类型为non-int
float health (const GameCharacter &gc) const;
};
class EvilBadGuy:public GameCharacter{  //同前
。。。
};
//另一个人物类型,假设构造函数与EvilBadGuy同
class EyeCandyCharacter:public GameCharacter{  
。。。
};
//人物1,使用某个函数计算健康指数
EvilBadGuy ebg1(calchealth);
//人物2,使用某个函数对象计算健康指数
EyeCandyCharacter ecc1(healthcalculator());
//人物3,使用某个成员函数计算健康指数
GameLevel currentLevel;
EvilBadGuy ebg2(std::trl::bind(&GameLevel::health, currentLevle, _1) ));
示例8-4-5 healthValue的Function Pointers手法的应用弹性
“_1”意味“当ebg2调用GameLevel::health时系以currentLevel作为GameLevel对象“。

?    古典的Strategy模式。
典型的Strategy会将健康计算做成一个分离的继承体系中的virtual成员函数。
 
图例8-4-1 类设计图
如果你未精通UML符号,这图的意思是:GameCharater是某个继承体系的根类,体系中EvilBadGuy 和EyeCandyCharacter都是derived classes;HealthCalcFunc是另一个继承体系的根类,体系中SlowHealthLoser和FastHealthLoser都是derived classes。每一个GameCharater对象都内含一个指针,指向来自HealthCalcFunc继承体系的对象。
class GameCharacter;   //前置声明
class  HealthCalcFunc{  
public:
  virtual int calc(const GameCharater & gc) const
{…}
};
HealthCalcFunc defaultHealthCalc;

class GameCharacter {
public:
explicit GameCharacter(HealthCalcFunc phcf = &defaultHealthCalc)
:pHealthFunc(phcf)
{}
int healthVaule() const
{ reurn pHealthFunc->calc()(*this);}
private:
HealthCalcFunc *pHealthFunc;
};
示例8-4-6 healthValue的Function Pointers手法的应用弹性
这个解法的吸引力在于,熟悉标准Strategy模式的人很容易辨认它,而它还提供“将一个既有的健康算法纳入使用“的可能性——只要为HealthCalcFunc继承体系添加一个derived classes即可。


8.5 条款36:绝不重新定义继承而来的non-virtual函数 (Never redefine a function 's inherited non-virtual function)

假设class D系由class B以public形式派生而来。
class Base {
public:
void mf();

};
class Derived:public Base{
public:
void mf(); //遮掩了B::mf,条款33

};      
D x;
B* pB = &x;
D* pD = &x;
pB->mf(); //调用B::mf
pD->mf(); //调用D::mf
示例8-5-1  名称遮掩
造成次一两面行为的原因是,non-virtual函数如B::mf和D::mf都是静态绑定(statically bound,条款37)。而virtual函数却是动态绑定(dynamicallly bound,条款37)。如果mf是个virtual函数,不论通过pB或pD调用mf,都会导致调用D::mf,因为pB和pD真正指的都是类型为D的对象。
所以,绝不重新定义继承而来的non-virtual函数。如果mf是B的一个non-virtual函数,B的derived classes一定会继承mf的接口和实现。

8.6 条款37:绝不重新定义继承而来的缺省参数值(Never redefine a function 's inherited default parameter value)

对象的所谓静态类型(static type),就是它在程序中被声明时所采用的类型。
class Shape {
public:
enum ShapeColor{Red, Green, Blue} ;
virtual void draw(ShapeColor color = Red) const = 0;

};
class Rectangle:public Shape {
public:
virtual void draw(ShapeColor color = Green) const; //糟糕

};      
class Circle:public Shape {
public:
virtual void draw(ShapeColor color) const ;
//请注意:当客户以对象调用此函数,一定要指定参数值,
//因为静态绑定下这个函数并不从其base继承缺省参数
//但是若以指针或reference调用次函数,可以不知道参数值
//因为动态绑定下这个函数会从其base继承缺省参数

};     
Shape *ps;    //静态类型为shape*
Shape *pc = new Circle; //静态类型为shape*
Shape *pr = new Rectangle; //静态类型为shape*

D* pD = &x;
pB->mf(); //调用B::mf
pD->mf(); //调用D::mf
示例8-6-1  静态类型与动态类型
本例中,ps,pc和pr都被声明为pointer-to-shape类型,所以他们都以它为静态类型,不论它们真正指向什么,他们的静态类型都是shape*。
对象的所谓动态类型(dynamic type)则是指“目前所指对象的类型“。上例中,pc的动态类型是Circle*,pr的动态类型是Rectangle*,而ps没有动态类型,因为它尚未指向任何对象。
动态类型可以在程序执行过程中改变(通常是经由赋值动作)。
ps = pc;   //ps的动态类型是Circle*
ps = pr;   // ps的动态类型是Rectangle*
virtual函数系动态绑定而来,意思调用一个virtual函数时,究竟调用哪份函数实现代码,取决于发出调用的那个对象的动态类型。
pc->draw(Shape::Red);   //调用Circle::draw(Shape::Red)
pr->draw(Shape::Red);   //调用Rectangle::draw(Shape::Red)
Virtual函数时动态绑定,而缺省参数却是静态绑定。 意思是调用一个定义域derived class内的virtual函数的同时,却使用了base class为它所指定的缺省参数值:
pr->draw();          //调用Rectangle::draw(Shape::Red),
//本意是Rectangle::draw(Shape:: Green)
以上事实不只局限于ps,pc和pr是指针情况,也符合reference的情况。

8.7 条款38:通过复合塑模出has-a或“根据某实物实现出” (Mode "has-a" or  " is-implementation-in-terms-of" through composition)

复合(composition)是类型之间的一种关系,当某种类型的对象内含它种关系类型的对象,便是这种关系。
程序中的对象其实相当于你所塑造的世界中的某些事物,例如人,气场,一张张视频画面等,这样的对象属于应用域(application domain)。其他对象则纯粹是实现细节上的人工制品,像是缓冲区,互斥器和查找树等等,这些对象相当于软件的实现域(implementation domain)。在应用域,复合意味has-a(有一个)。在实现域,复合意味is-implementation-in-terms-of(根据某物实现出)。
假设你需要一个template,希望做一组classes用来表示由不重复对象组成的sets。直觉是采用STL的set  template。但是,set的实现往往招致每个元素好用三个指针,因为sets以平衡查找树实现而成,使他们在查找、安插、移除元素时保证拥有对数时间效率。但是空间有限,只好自己设计一个set template,底层采用linked lists。而此时就是复用std::list。也就是set<T>继承list<T>。但是list可以含有重复元素,而set不能含有重复元素。因此,“set是一种list“并不是真。也就不能用public继承塑模它们。而正确的做法是,set对象可 根据一个list对象实现出来。这就是is-implementation-in-terms-of。

8.8 条款39:明智而审慎地使用private继承(Use private inheritance judiciously)

大多数继承相当于is-a,这是指public继承,不是private继承。复合和private继承都意味着is-implementation-in-terms-of,但是复合交易理解,所以无论什么时候,只要可以,你还是应该选择复合。当你面对“并不存在is-a关系“的两个classes,其中一个需要访问另一个的protected成员,或需要重新定义一个或多个virtual函数,private继承就派上用场。还有一种激进情况,当你所处理的class不带任何数据,也可以使用private继承。
第一:如果classes之间的继承关系是private,编译器不会自动将一个derived class对象(例如Student)转换为一个base class对象(例如Person)。第二:由private base class继承而来的所有成员,在derived class中都会变成private属性,纵使它们在base class中原本是protected或public属性。
Private继承纯粹只是一种实现技术,在软件“设计“层面上没有意义,其意义只及于软件实现层面。
假设我们的程序涉及Widgets,还需要一个统计功能。为了让Widget重新定义Timer内的虚函数,Widget必须继承自Timer,但是public在此例不适当,因为Widget并不是一个Timer,如下:
class Timer{
public:
explicit Timer(int tickFrequency);
virtual void onTick() const; //定时器每滴答一次,此函数自动被调用一次

};  
class Widget:private Timer{
private:
virtual void onTick() const; //借由private继承,Timer的public属性
//变成private,而我们放在private内,避免误导客户

};    
示例8-8-1  private继承
如果我们觉得以复合取而代之,也是可以的,如下:
class WidgetTimer:public Timer{
public:
virtual void onTick() const;

};  
class Widget{
private:
WidgetTimer timer;

};    
示例8-8-2  复合
我们之所以选在复合,原因如下
?    你或许会想设计Widget使它得以拥有derived classes,但是同时你可能会想阻止derived classes 重新定义onTick。
如果Widget继承自Timer,上面的想法就不可能实现,即使是private继承也不行。
?    你或许会想要将Widget的编译依存性降至最低。
如果Widget继承Timer,当Widget被编译时Timer的定义必须可见。
但是你所处理的class不带任何数据,偏向使用private继承。

class Empty {};    //没有数据
//应该只需要一个int空间
class HoldsAnInt:private Empty{  
private:       
int x;

};      class Empty {};    //没有数据
class HoldsAnInt{  //应该只需要
private:        //一个int空间
int x;
Empty e;

};  
示例8-8-3(a) private继承           示例8-8-3(b))组合
在8-8-3(b)中,sizeof(HoldsAnInt) > sizeof(int),原因是大多数编译器sizeof(Empty)获得1,面对“大小为零之独立(非附属)对象“,C++官方勒令默默安插一个char到空对象中。
在8-8-3(a)中,sizeof(HoldsAnInt) =sizeof(int),这是所谓的EBO(empty base optimization,空白基类最优化)。EBO一般只在单一继承下才可行。
但是,现实的“empty“ classes并不是真的是Empty。虽然它们从未拥有non-static成员变量,却往往内含typedef,enums,static成员变量,或non-virtual函数。
这是与复合的不同之处,private继承可以造成empty classes base最优化。这对于”对象尺寸最小化“的程序开发者而言,可能很重要。

8.9 条款40:明智而审慎地使用多重继承 (Use multiple inheritance judiciously)

多重继承需要清楚的一件事,当MI(multiple inheritance)进入设计景框,程序有可能从一个以上的base classes继承相同的名称(如函数,typedef等)。它可能导致新的歧义性,以及对virtual继承的需要。比如,钻石型多重继承。
 
图例8-9-1  钻石型多重继承
假设File类有成员变量filename,那IOFile该有多个份这个名称的数据呢?从继承来说,IOFile对象只该有一个文件名称。为了这样做,你必须令所有直接继承base class的derived class采用“virtual继承”。

class File{};
class InputFile: virtual public File{};
class OutputFile: virtual public File{};
class IOFile: virtual public InputFile, virtual public OutputFile {};  
示例8-9-1  virtual继承
为了避免继承得来的成员变量重复,编译器必须提供诺干戏法,而其后果是:使用virtual继承的那些classes所产生的对象往往比使用non-virtual继承的兄弟们体积大,访问virtual base classes的成员变量时,也比访问non-virtual base classes的成员变量速度慢。Virtual base的初始化责任是由继承体系的最底层class负责。
所以,我们的忠告是:第一,非必要不要使用virtual base。第二,如果必须使用virtual base classes,尽可能避免在其中放置数据。

class IPerson {
public:
virtual ~IPerson();
virtual std:string name() const = 0;
virtual std:string birthDate() const = 0;
};
class DatabaseID{…};
class PersonInfo {
public:
explicit PersonInfo(DatabaseID pid);
virtual ~PersonInfo();
virtual const char* theName() const;
virtual const char* theBirthDate() const;
virtual const char* valueDelimOpen() const;
virtual const char* valueDelimClose() const;

};
const char* PersonInfo::valueDelimOpen() const
{
    return "[";
}
const char* PersonInfo::valueDelimClose() const
{
    return "]";
}

const char* PersonInfo::theName() const
{
   static char value[NAME_LEN_MAX];
std::strcpy(value, valueDelimOpen());
… //将value的字符串添加到这个对象的name成员变量中
std::strcat(value, valueDelimClose());
return value;
}
class CPerson:public IPerson,private PersonInfo{
public:
explicit CPerson(DatabaseID pid):PersonInfo(pid){}
//实现必须的IPerson的成员函数
virtual std::string name const
{ return PersonInfo::theName();}
virtual std::string birthDate const
{ return PersonInfo::theBirthDate();}
private:
//重新定义继承而来的virtual函数
const char* valueDelimOpen() const {return  "";}
const char* valueDelimClose() const{ return  "";}
};

示例8-9-2  多重继承
IPerson类是抽象类,只提供接口。早有PersonInfo类的存在,可以为CPerson类提供所需的实质东西。CPerson和PersonInfo的关系是is-implementation-in-terms-of。而实现这种关系有两种技术:复合和private继承。复合虽然比较受欢迎,但是如果需要重新定义virtual函数,那么继承是必要的。本例需要重新定义valueDelimOpen和valueDelimClose,所以单纯的复合无法应对。但是CPerson也必须实现Iperson接口,那需得以public继承才能完成。
所以,多重继承的正当用途之一是,涉及“public继承某个Interface class”和“private继承某个协助实现的class”的两项组合。


發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章