条款32-40

条款32:确定你的public继承塑模出is-a关系

        public inheritance意味“is-a”(是一种)的关系。如果令class D以public继承class B,即每一个类型为D的对象同时也是一个类型为B的对象,反之不成立。B比D表现出更一般化的概念,而D比B表现出更特殊化的概念B可以派上用场的地方,D对象一样可以派上用场,因为每一个D对象都是一个(是一种)B对象。

请记住:

        “public继承”意味着is-a。适用于base class身上的每一件事情一定也适用于derived class身上,因为每一个derived class对象也是一个base class对象


条款33:避免遮掩继承而来的名称

        derived class作用域被嵌套在base class作用域内

class base{
private:
int x;
public:
virtual void mf1()=0;
virtual void mf2();
void mf3();
...
};
class derived:public base{
public:
virtual void mf1();
void mf4;
...
};
void derived::mf4()
{
...
mf2();
...
}

        编译器看到mf2,首先查找local作用域(即mf4作用域),然后查找外围作用域,也就是derived class覆盖的作用域,再继续往外围移动,查找base class作用域,找到mf2于是停止查找。如果base class内没有mf2,便继续查找,首先查找base class的那个namespace的作用域,最后往global作用域。

class base{
private:
int x;
public:
virtual void mf1()=0;
virtual void mf1(int);
virtual void mf2();
void mf3();
void mf3(double);
...
};
class derived:public base{
public:
virtual void mf1();
void mf3();
void mf4;
...
};

        base clas内所有名为mf1和mf3的函数都被derived class内的mf1和mf3函数掩盖了。从名字查找规则来看base::mf1和base::mf3不再被derived继承。

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

        可见base class和derived class内的函数有不同参数类型也适用,而且不论函数时virtual还是non_virtual一体适用,如今derived内的函数mf3掩盖了一个名为mf3但类型不同的的base函数。

        如果正在使用public继承而又不继承那些重载函数,就是违反base和derived class之间的is-a关系,可以使用using声明达成目标:

class base{
private:
int x;
public:
virtual void mf1()=0;
virtual void mf1(int);
virtual void mf2();
void mf3();
void mf3(double);
...
};
class derived:public base{
public:
using base::mf1;            //让base class内名为mf1和mf3的所有东西在derived作用域内都可见
using base::mf3;
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

        同一作用域内,相同函数名才是函数的重载,需要重载确定寻找最佳匹配,不同作用域,不存在重载,如嵌套作用域意味着函数的覆盖(个人总结,不知道有没有问题)

        如果并不想继承base class所有函数,但这在public继承下,绝不可能发生,它违背了public继承的is-a关系,但是在private继承下是有意义的。如果单一想继承mf1的无参数版本,using声明派不上用场,因为using声明会令所有同名函数皆可见,这时就需要一个转交函数:

class base{
private:
int x;
public:
virtual void mf1()=0;
virtual void mf1(int);
//同前
};
class derived:private base{
public:
virtual void mf1()      //转交函数
{
base::mf1();
}
...
};
derived d;
int x;
d.mf1();      //ok,derived::mf1
d.mf1(x);     //error,base::mf1(int)被遮掩
请记住:

        derived class内的名称会遮掩base class内的名称。在public继承下从来没有人希望如此

        为了让被遮掩的名称再重见天日,可使用using声明式或转交函数


条款34:区分接口继承和实现继承
class shape{
public:
virtual void draw()const=0;
virtual void error(const std::string &msg);
int objectID()const;
...
};
class rectangle:public shape{...};
class ellipse:public shape{...};
声明一个pure virtual函数的目的是为了让derived class只继承函数接口

        pure virtual函数必须被任何继承了它们的具象class重新声明,而且它们在抽象class中通常没有定义。但我们可以为pure virtual函数提供定义,但调用它的唯一途径是调用时明确指出其class的名称

shape *ps=new shape;      //错误,shape是抽象的
shape *ps1=new rectangle
shape *ps2=new ellipse;
ps1->shape::draw();       //调用shape::draw
ps2->shape::draw();       //调用shape::draw
声明一个impure virtual函数的目的是为了让derived class继承函数接口和缺省实现
class shape{
public:
virtual void error(const std::string &msg);
...
};

        derived class的设计者必须支持一个error函数,如果不想自己写一个,可以使用shape class提供的缺省版本。

声明一个non-virtual函数的目的是为了让derived class继承函数接口及一份强制实现

        对于shape::objectID任何derived class都不应该尝试改变其行为,对于non-virtual函数,不变形凌驾特异性,所以它绝不该在derived class中被重新定义。

请记住:

        接口继承和实现继承不同,在public继承之下,derived class总是继承base class的接口

        pure virtual函数只具体指定接口继承

        impure virtual函数具体指定接口继承以及缺省实现继承

        non-virtual函数具体指定接口继承以及强制性实现继承


条款35:考虑virtual函数以外的其他选择
藉由non-virtual interface手法实现template method模式

        这个流派主张virtual函数应该几乎总是private,较好的设计是保留healthvalue为public成员函数,但让它成为non-virtual,并调用一个private virtual函数进行实际工作:

class gamecharactor{
public:
int healthvalue()const
{
...                                //做一些事前工作
int retval=dohealthvalue();        //做真正工作
...                                //做一些事后工作
return retval;
}
...
private:
virtual int dohealthvalue()const    //derived class可以重新定义自己的计算函数
{
...
}
};

        这一基本设计令客户通过public non-virtual成员函数间接调用private virtual函数,成为non-virtual interface(NVI)手法。我们把non-virtual函数称为virtual函数的外附器(wrapper)

藉由function pointer实现strategy模式

        NVI手法中我们还是需要使用virtual函数来计算每个人物的健康指数。我们可以要求每个人物的构造函数接受一个指针,指向一个健康计算函数,而我们可以调用该函数进行实际计算:

class gamecharactor;      //前置声明
int defaulthealthcalc(const gamecharactor &gc);
class gamecharactor{
public:
typedef int (*healthcalcfunc)(const gamecharactor&);
explicit gamecharactor(healthcalcfunc hcf=defaulthealthcalc):healthfunc(hcf)
{}
int healthvalue()const
{ return healthfunc(*this);}
...
private:
healthcalcfunc healthfunc;
};

  • 同一人物类型的不同实体可以有不同的健康计算函数:
int losehealthquickly(const gamecharactor&);
int losehealthslowly(const gamecharactor&);
class evilbadguy:pulic gamecharactor{
public:
explicit evilbadguy(healthcalcfunc hcf=defaulthealthcalc):gamecharactor(hcf)
{}
};
evilbadguy ebg1(losehealthquickly);
evilbadguy ebg2(losehealthslowly);

  • 某已知人物的健康计算函数可在运行期间改变:

        健康指数计算函数不再是gamecharactor继成体系内的成员函数,这意味着这些计算函数并未特别访问即将被计算健康指数的那个对象的内部成分,如果需要non-public信息进行计算就存在问题。解决办法是弱化class封装,将non-member函数声明为friend。

藉由tr1::function完成strategy模式

        由上一条我们还可以优化,不必必须使用一个函数,而是可以为象函数的某个东西,改用tr1::function的对象,这些约束就不存在了,这些东西可以是函数指针、函数对象或成员函数指针:

class gamecharactor;
int defaulthealthcalc(const gamecharactor &gc);
class gamecharactor{
public:
typedef std::tr1::function<int (const gamecharactor&)> healthcalcfunc;
explicit gamecharactor(healthcalcfunc hcf=defaulthealthcalc):healthfunc(hcf)
{}
int healthvalue()const
{ return healthfunc(*this);}
...
private:
healthcalcfunc healthfunc;
};

        与上一种方法不同的是,如今的gamecharactor持有一个tr1::function对象,相当于一个指向函数的泛化指针,这意味着可调用物的参数可以被隐式转换为const gamecharacter&,其返回值可被隐式转换为int,这使得其变得具有惊人的弹性:

short calchealth(const gamecharactor&);      //其返回类型为non-int
struct healthcalculator{
int operator()(const gamecharactor &)const
{...}
};
class gamelevel{
public:
float health(const gamecharactor &)const;
...
};
class evilbadguy:public gamecharactor{
//同前
};
class eyecandycharacter:public gamecharacter{
...      //另一个人物类型
};
evilbadguy ebg1(calchealth);      //使用某个函数计算
eyecandycharacter ecc1(healthcalculator());      //使用某个函数对象计算
gamelevel currentlevel;
evilbadguy ebg2(std::tr1::bind(&gamelevel::health,currentlevel,_1));

        这里解释一下bind。gamelevel::health显示接受一个参数,但它却接受两个参数,包括一个隐式参数gamelevel,然而gamecharacter的健康计算函数只接受一个参数,如果我们使用gamelevel::health,就必须转换它,这里我们想到使用currentlevel作为ebg2健康计算函数所需的那个gamelevel对象(这里跳过了一些细节,只叙述了使用bind原因)。

古典的strategy模式

        这种做法将健康计算函数做成一个分离的继成体系内的virtual成员函数,每一个gamecharacter对象都内含一个指针,指向一个来自healthcalcfunc继承体系的对象:

class gamecharacter;
class healthcalcfunc{
public:
virtual int calc(const gamecharacter &gc)const
{...}
...
};
healthcalcfunc defaulthealthcalc;
class gamecharacter{
public:
explicit gamecharacter(healthcalcfunc *phcf=&defaulhealthcalc):phealthcalc(phcf)
{}
int healthvalue()const
{return phealthcalc->calc(*this);}
private:
healthcalcfunc *phealthcalc;
};
请记住:

        virtual函数的替代方案包括NVI手法以及strategy设计模式的多种形式,NVI手法自身是一个特殊形式的template method设计模式

        将机能从成员函数移到class外部函数,带来一个缺点是,非成员函数无法访问class的non-public成员

        tr1::function对象的行为就像一般的函数指针。这样的对象可接纳与给定目标签名式兼容的所有可调用之物


条款36:绝不重新定义继承而来的non-virtual函数

        如之前所说public继承意味着is-a的关系,在class内声明一个non-virtual函数会为该class建立一个不变性凌,驾于特异性之上。


条款37:绝不重新定义继承而来的缺省参数值

        virtual函数是动态绑定,而缺省值参数是静态绑定

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)const;      //注意,以上这么写则当用户以对象调用此函数,一定要指定参数值,因为静态绑定下这个函数并不从base继承缺省参数
                                         //但若以指针(或reference)调用此函数,可以不指定参数值,因为动态绑定下这个函数会从其base继承缺省参数值
};
shape *ps;
shape *pc=new circle;
shape *pr=new rectangle;

        本例中ps、pc和pr都被声明为pointer-to-shape所以它们的静态类型都是shape*,所谓动态类型则是指目前所指对象的类型

pr->draw();      //调用rectangle::draw(shape::red)

        pr的动态类型是rectangle*,所以调用的是rectangle的virtual函数,rectangle::draw的缺省参数因该是green,但由于pr的静态类型是shape*,所以调用的缺省参数值来自shape class而非rectangle class。C++之所以以这种方式运作,是因为如果缺省参数值动态绑定,程序执行速度和编译器实现上的难度更大。

        当我们尝试遵守此条,并将所有缺省参数赋为相同值时,造成了代码的重复,重复就意味着相依性,如果shape内的缺省参数值改变了,所有给定缺省参数值的代码都需要改写,则是特别麻烦的。解决办法就是前一条所述的virtual函数的替代方法

请记住:
        绝不要重新定义一个继承而来的缺省参数值,因为缺省参数值都是静态绑定的,而virtual函数却是动态绑定的

条款38:通过复合塑模出has-a或根据某物实现出

        复合是一种类型之间的关系,当某种类型对象内含它种类型的对象

        复合意味着has-a(有一个)或is-implemented-in-terms-of(根据某物实现出)。当对象属于应用域(application domain),表现出has-a的关系;当对象属于实现域,表现出is-implemented-in-terms-of的关系


条款39:明知而审慎的使用private继承

        如果class之间的继承关系是private,编译器不会自动将一个derived class对象转换为一个base class对象

        private继承意味着implemented-in-terms-of(根据某物实现出),只有部分实现被继承,接口部分应略去

        这与复合的意义相同,一般来说,尽可能使用复合,必要时才使用private继承,那就是空间利害关系足以踢翻private继承支柱时:

        class不带任何数据时,这样的class没有non-static成员变量,没有virtual函数(这会产生一个vptr),也没有virtual base class(这会导致体积上的额外开销)。于是,这种empty class对象不使用任何空间,但C++裁定凡是独立(非附属)对象都必须有非零大小

class holdanint:private empty{
private:
int x;
};

        我们会发现sizeof(holdanint)>sizeof(int),但是对于非独立的对象,上调并不适用:

class empty{};
class holdanint:private empty{
private:
int x;
empty e;
};

        这时sizeof(holdanint)=sizeof(int),这是所谓的EBO(empty base optimization;空白基类最优化),这时会选择private继承而不是继承加复合。private继承可由继承加复合取代:在类中private部分定义一个新的类,该类public继承我们想要private继承的类。

        这两种方法有区别,这也是决定使用哪种方法的因素:private继承的derived class可以重新定义virtual函数,而继承加复合不行。因为新的类是base class的private成员并继承自另一个类,derived class不能获取新定义的类,也就无法继承它或重新定义virtual函数

请记住:

        private继承意味着is-implemented-in-terms-of,它通常比复合的级别低。但是当derived class需要访问protected base class的成员,或需要重新定义继承而来的virtual函数时,可么设计是合理的

        和复合不同,private继承可以造成empty base最优化。这对致力于对象尺寸最小化的程序可开发者而言可能很重要


条款40:明智而审慎地使用多重继承
class borrowableitem{
public:
void checkout();
...
};
class electronicgadget{
private:
bool checkout()const;
...
};
class MP3player:public borrowableitem,public electronicgadget
{...};
MP3player mp;
mp.checkout();           //歧义,调用哪个checkout

        此例中对于checkout的调用是歧义的,即使两个中只有一个可用(borrowableitem内的checkout是public,electronicgadget内的却是private),这与C++用来解析重载函数的调用规则有关:在看到是否有个函数可调用之前,C++首先确认这个函数对此调用而言是最佳匹配。即找到最佳匹配之后才检验其可取性

        多重继承还会导致base class在derived class中有多分其成员变量。这是C++的缺省做法,不想如此,我们就必须运用virtual继承:

class file{...};
class inputfile:virtual public file{...};
class outputfile:virtual public file{...};
class iofile:public inputfile,public outputfile
{...};

        从行为正确的观点看,将public继承总是设为virtual是正确的。但正确性并不是唯一的观点,使用virtual继承的那些class所产生的对象往往比non-virtual继承要大,访问成员变量也更加慢,这就是为保证正确性付出的代价。

        并且virtual继承还导致virtual base class的初始化是由继承体系中的最底层class负责。因此,建议尽量不使用virtual继承,如果必须使用virtual继承,尽量避免在virtual base class中存放数据,这样就不用担心在这些class上的初始化问题。

        但多重继承有其用途,书中有例子就不赘述。

请记住:

        多重继承比单一继承复杂,它可能导致新的歧义性,以及对virtual继承的需要

        virtual继承会增加大小、速度、初始化复杂程度等等成本。如果virtual base class不带任何数据,将是最具实用价值的情况

        多重继承有正当的用途。其中一个情节涉及public继承某个interface class和private继承某个协助实现的class的两相组合

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值