effective c++ 读书笔记(六)

本文探讨了C++中的继承概念,包括公共继承、接口继承与实现继承、虚拟函数的选择,以及如何避免遮蔽继承名称。文章强调了在继承时遵循is-a原则的重要性,并介绍了使用private继承和多重继承的策略。此外,还讨论了非虚拟接口(NVI)模式、策略模式以及复合优于继承的原则,以帮助实现更高效的设计。
摘要由CSDN通过智能技术生成

继承与面向对象设计

继承可以是单一继承或多重继承,每一个继承链接可以是public、protected、private也可以是virtual或non-virtual的。各个成员函数的各个选项:virtual?non-virtual?pure virtual?以及成员函数和其他语言特性的交互影响:缺省参数值与virtual函数有什么交互影响?继承如何影响c++名称查找规则?设计选项有哪些?如果class的行为需要修改,virtual函数是最佳选择嘛?
public继承意味者is-a,virual函数意味着接口必须被继承,non-virtual意味这接口和实现必须都被继承。

确定你的public继承塑膜出is-a关系

“Base 对象可派上用场的任何地方,Derived 对象一样可以派上用场,反之不成立liskov substitution principle”,每个Derived对象都是一个Base对象。
public继承主张,能够施行于base class对象身上的每件事,也可以施行于derived class对象身上。使用public继承,你就必须继承base内的所有。
像正方形是矩阵这样的存在与我们大脑中直观印象,并不适用于面向对象public继承关系下。

避免遮蔽继承而来的名称

一般局部变量会遮蔽外围作用域的名称,至于名称是否相应和相同或不同的类型,并不重要。
继承的时候,derived class成员函数refer to base class内的某物时,编译器可以找出我们指向的东西,因为derived class继承了声明与base class内的所有东西。实际方式是,derived class作用域被嵌套在base class 作用域内。
这里写图片描述
上图例子中包含了private,public名称,同时成员函数包括virtual ,pure virtual和non-virtual三种。是为了说明名称和其他无关。

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();
    ///
};

Derived d;
int x;
d.mf1(); //true
d.mf1(int); //false  因为Derived::mf1 遮蔽了Base::mf1
d.mf2(); //true
d.mf3(); //ture
d.mf3(x); //false  因为Derived::mf3遮蔽了Base::mf3

上述行为是为了防止在建立新的derived class时附带的从疏远的base class继承重载函数,但是有的时候你想要这么做,实际上如果使用public继承又不继承那些重载函数,就是违反了base和derived class之间的is-a的关系。可以通过使用using方法来达到目标:

//可以达到继承base class内的所有函数的目的,不至于破坏is-a的关系
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;
    using Base::mf3;
    virtual void mf1();
    void mf3();
    void mf4();
    ///
};

上述的方法达到:你可以继承base class并加上重载函数,同时又可以重新定义或覆写其中一部分,那么可以为原本会被遮蔽的每个名称引入一个using声明式,否则某些希望继承的名称就会被遮蔽
当不希望继承所有base class内的东西时,可以通过private继承来完成。当希望继承base内的一个函数时,可以通过转交函数来实现。

class Base{
public:
    virtual void mf1() = 0;
    virtual void mf1(int);
    //...与前面相同
};
class Derived: private Base{
public:
    virtual void mf1(){ Base::mf1();}  //转交函数自动成为inline函数
    //...
};

Derived d;
int x;
d.mf1();  //true 调用的是Derived::mf1()
d.mf1(x); //false  Base::mf1()被遮蔽了

区分接口继承和实现继承

public继承一般是由两部分组成:函数接口继承和函数实现继承,两者的差异很像函数声明和函数定义之间的差别。
(继承:只继承函数的接口(即声明),继承函数的接口和实现,但又能够覆写他们所继承的实现;同时继承接口和实现并且不允许覆写任何东西)

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只继承函数接口,继承的类必须重新定义。但是在调用的时候必须明确指出其class名称。

Shape* ps = new Shape; //false  Shape 是抽象的
Shape* ps1 = new Rectangle; //true
ps1->draw(); //调用的是Rectangle::draw();
Shape* ps2 = new Ellipse; //true
ps2->draw(); //调用的是Ellipse::draw();
ps1->Shape::draw(); //调用的是Shape::draw()

声明impure virtual函数的目的,是让derived class继承该函数的接口和缺省实现,derived class可能覆写它
但是允许impure virtual函数同时指定函数声明和函数缺省行为,却有可能造成危险

class Airport{ //...};
class Airplane{
public:
    virtual void fly(const Airport& destination);
    //...
};
void Airplane::fly(const Airport& destination){
    //缺省代码,将飞机飞至指定目的地
}
class ModelA: public Airplane{ //...};
class ModelB: public Airplane{ //...};
//两个class共享一份相同性质fly的实现。
//当新增一辆飞机时,它的飞行方式不同与AB,但是忘记重新定义了fly函数
class ModelC: public Airplane{ //...}; //未声明fly函数

Airport PDX(//...);
Airplane *pa = new ModelC;
pa->fly(PDX);  //false 因为c的飞行方式不同于AB

因为ModelC在未明白说出“我要”的情况下就继承了缺省行为,导致了错误。可以通过切断virtual函数接口和其缺省实现之间的连接方法来避免。

class Airplane{
public:
    virtual void fly(const Airport& destination) = 0;
    //...
protected:
    void defaultFly(const Airport& destination);  //没有任何一个derived class应该重新定义此函数
    //不能声明为virtual函数,因为derived class可能忘记重新定义它
};
void Airplane::defaultFly(const Airport& destination){
    //缺省行为,将飞机飞至指定目的地
}

class ModelA: public Airplane{
public:
    virtual void fly(const Airport& destination){
        defaultFly(destination);
    }
};
class ModelC: public Airplane{
public:
    virtual void fly(const Airport& destination);
};
void ModelC::fly(const Airport& destination){
    //将飞机C飞至指定目的地
}

还可以通过pure virtual函数来完成上述工作:

class Airplane{
public:
    virtual void fly(const Airport& destination) = 0;
};
void Airplane::fly(const Airport& destination){
    //缺省行为,将飞机飞至指定目的地
}

class ModelA: public Airplane{
public:
    virtual void fly(const Airport& destination){
        Airplane::fly(destination);
    }
};
class ModelC: public Airplane{
public:
    virtual void fly(const Airport& destination);
};
void ModelC::fly(const Airport& destination){
    //将飞机C飞至指定目的地
}

**声明non-virtual函数的目的是为了令derived class继承函数的接口及一份强制性实现。**non-virtual函数代表的意义是不变形凌驾与特异性。

考虑virutal函数以外的其他选择

借由non-virtual interface 手法实现template method 模式

class GameCharacter{
public:
    int healthValue()const{
        //....  做一些事前工作
        int retVal = doHealthValue();   //做真正的工作
        //.... 做一些事后工作
        return retVal;
    }
    //...
private:
    virtual int doHealthValue() const{
        //...缺省算法
    }
};
//non-virtual 函数称为virtual函数的外敷器(wrapper)

优点:确保得以在一个virtual函数被调用前设定好适当的场景,并在调用结束后清理场景。
NVI手法涉及在derived class内重新定义private virtual函数的。NVI手法允许derived class重新定义virtual函数,从而赋予他们“如何实现技能”的控制能力,但是base class保留述说“函数何时被调用”的权利。(NVI手法下没有必要让virtual函数一定是private的)

藉由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 healthValue() const{ return healthFunc(*this); }
    //...
private:
    HealthCalcFunc healthFunc;
};
//同一人物类型的不同实体可以有不同的健康计算函数
class  EvilBadGuy: public GameCharacter{
public:
    explicit EvilBadGuy(HealthCalcFunc hcf = defaultHealthCalc): GameCharacter(hcf){ //...}
    //...
};
int loseHealthQuickly(const GameCharacter&);
int loseHealthSlowly(const GameCharacter&);
EvilBadGuy ebg1(loseHealthQuickly);
EvilBadGuy ebg2(loseHealthSlowly);

//某已知人物的健康指数计算函数可以在运行期变更。例如:GameCharacter可提供一个成员函数setHealthCalculator,用以替换当前的健康指数计算函数。
//换句话说,健康指数计算函数不再是GameCharacter继承体系内的成员函数。意味着,这些计算函数并未特别访问“即将被计算健康指数”的那个对象的内部成份。

当需要以non-member函数访问class 的non-public成员的方法是:弱化class 的封装。可以将那个non-member函数声明为class 的friend。或为其实现的某一部分提供public访问函数。用函数指针替换virtual函数的优点(像每个对象都有自己的健康计算函数)是否弥补缺点(降低class的封装性),是必须根据每个设计情况的不同而抉择的

藉由tr1::function完成strategy模式

class GameCharacter;   //前置声明
int defaultHealthCalc(const GameCharacter& gc)// 此函数计算健康指数的缺省算法
class GameCharacter{
public:
    typedef std::tr1::function<int (const GameCharacter&)>HealthCalcFunc;
    explicit GameCharacter(HealthCalcFunc hcf = defaultHealthCalc): healthFunc(hcf){}
    int healthValue() const{ return healthFunc(*this); }
    //...
private:
    HealthCalcFunc healthFunc;
};

tr1::function的签名代表的函数是“接收一个reference指向const GameCharacter,并返回int”,这个tr1::function类型产生的对象可以持有任何与此签名式兼容的可调用物。(兼容是值可调用物的参数可被隐式转换为cons GameCharacter&,而其返回类型可被隐式转换为int)。

short calcHealth(const GameCharacter&);
struct HealthCalculator{
    int operator()(const GameCharacter&) const{ //...}
};
class GameLevel{
public:
    float health(const GameCharacter&) const;
    //...
};
class  EvilBadGuy: public GameCharacter{  //同上
    //...
};
class  EyeCandyCharacter: public GameCharacter{  //同上
    //...
};

EvilBadGuy ebg1(calcHealth); //人物1
EyeCandyCharacter ecc1(HealthCalculator());  //人物2
GameLevel currentLevel;
EvilBadGuy ebg2(std::tr1::bind(&GameLevel::health, currentLevel, _1)); //具体细节需要另说

以上的方法只是一个开头,其他很多实现设计方法需要探索。

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

class B{
public:
    void mf();
};
class D:public B{ //...};

D x;
B* pb = &x;
pb->mf();

D* pd = &x;
pd->mf(); 
//如果D中重新定义了non-virtual函数,那么B中的mf函数就被遮蔽啦,所以两者的调用结果不一样。

时刻记住public是is-a的关系,即适用于B对象的每一件事,也适用于D对象。B的derived class一定会继承mf的接口和实现

绝不重新定义继承而来的缺省参数值()

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 color) const;
    //当客户以对象调用此函数,一定要指定参数值
    //因为静态绑定下这个函数并不从其base继承缺省参数值
    //但若以指针(或引用)调用这个函数,可以不指定参数值,因为动态绑定下这个函数会从其base继承缺省参数值
};

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

pr->draw();  //调用Rectangle::draw(Shape::Red)
//pr的动态类型是Rectangle*,索引调用的是Rectangle 的virtual函数,Rectangle::draw函数的缺省参数值应该是Green,但由于pr的静态类型是Shape*,所以此调用的缺省参数值来自Shape class而不是Rectangle class
//导致调用很奇怪。

通过符合塑膜出has-a或“根据某物实现出”

class Address{};
class PhoneNumber{};
class Person{
public:
private:
    std::string name;
    Address address;
    PhoneNumber voiceNumber;
};

复合意味这has-a或is-implemented-in-terms-of(根据某物实现出)。在应用域上是has-a的意思,但是在实现域上复合表现为is-implemented-in-term-of的关系。
例如:当用list实现set的时候,如果使用has-a的关系,那么就会导致冲突

template<typename T>
class Set: public std::list<T>{};  //错误的用法,因为list允许元素重复,但是set不行。

此时应该采用is-implemented-in-term-of的关系

template<class T>
class set{
public:
    bool member(const T& item) const;
    void insert(const t& item);
    void remove(const T& item);
    std::size_t size() const;
private:
    std::list<T> rep;
};

明智而审慎的使用private继承()

如果class之间的继承关系是private,编译器不会自动将一个derived class对象转换为一个base class对象。由private base class继承而来的所有成员,在derived class中都会变成private 属性,纵使它们在base class中原本是protected或public属性。private继承意味这implemented-in-terms-of(根据某物实现出)
private继承纯粹是一种实现技术(这就是为什么继承子一个private base class的每样东西在你的class内都是private,因为他们都是实现枝节而已)。
如果D以private形式继承B,意思是D对象根据B对象实现而得,再没有其他意思啦
尽可能使用复合,必要时采用private继承,当protected成员和或virtual函数牵涉进来的时候,是必要。其中一个需要访问另一个的protected成员,或需要重新定义一个或多个virtual函数,private继承可能成为正统设计策略。
同时private继承可以造成empty base最优化。这对于致力于对象尺寸最小化的程序库开发者而言,可能很重要。

明智而审慎地使用多重继承()

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

class BorrowwableItem{
public:
    void checkOut();
};
class ElectronicGadget{
private:
    void checkOut();
};
class MP3Player: public BorrowableItem, public ElectronicGadget{
    //...
};

MP3Player mp;
mp.checkOut();  //false 歧义,不知道调用哪个checkOut
mp.BorrowableItem::checkOut();  //true

virtual继承会增加大小,速度,初始化复杂度等成本。如果virtual base class不带任何数据,将是最具使用价值的情况。
非必要不实用virtual base,平常使用non-virtual继承,第二,如果你必须使用virtual base class 尽可能避免在其中放置数据
这里写图片描述
以后补充深入探索c++对象模型的内存分讨论
多重继承的正当用途的一个例子:当public继承某个interface class和private继承某个协助实现的class的两相组合的情况下。

//用来塑膜人的interface class
class IPerson{
public:
    virtual ~IPerson();
    virtual std::string name() const = 0;
    virtual std::string birthDate() const = 0;
};
//IPerson的客户必须用IPerson的指针或reference来编写程序
//使用factory function可以将派生自IPerson的具象class实体化:
std::shared_ptr<IPerson> makePerson(DatabaseID persoonIdentifier);
//这个函数从使用这手上取得一个数据库ID
DatabaseID askUserForDatabaseID();
DatabaseID id(askUserForDatabaseID());
std::shared_ptr<IPerson> pp(makePerson(id));
lcass PersonInfo{
public:
    explicit PersonInfo(DatabaseID pid);
    virtual ~PersonInfo();
    virtual const char* theName() const;
    virtual const char* theBirthDate() const;
private:
    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[Max_formatted_field_value_length];
    std::strcpy(value, valueDelimOpen());
    std::strcat(value,valueDelimClose());
    return value;
}

为了代码复用,通过PersonInfo来实现CPerson
这里写图片描述

class CPerson: public IPerson, private PersonInfo{
public:
    explicit CPerson(DatabaseID pid):PersonInfo(pid){}
    virtual std::string name() const{ return PersonInfo::theName();}
    virtual std::string birthDate() const{ return PersonInfo::theBirthDate();}
private:
    const char* valueDelimOpen() const { return "";}
    const char* valueDelimClose() const { return "";}
};
  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值