【Effective C++】继承和面向对象设计

一、确定你的public继承塑模出is-a模型

1、public继承与“is-a”

  以C++进行面向对象编程,最重要的一个规则是:public inheritance(公开继承)意味“is-a”(是一种)的关系。请务必牢记。当你令class D 以public形式继承class B,你便是告诉C++编译器,每一个类型为D的对象同时也是一个类型为B的对象,反之不成立。你的意思是B比D表现出更一般化的概念,而D比B表现出更特殊化的概念。is-a关系只对public继承才成立。

class Person { ...  };
class Student : public Person { ... };

  有时public和is-a之间的关系会误导我们。例如,企鹅(penguin)是一种鸟,这是事实;鸟可以飞,这也是事实。如果以C++描述这层关系:

class Bird
{
    public:
        virtual void fly();    // 鸟可以飞
        .....
};
class Penguin:public Bird  // 企鹅是一种鸟
{
    .....
};

但是我们知道,企鹅不会飞,这个是事实。这个问题的原因是语言(英语)不严谨。当我们说鸟会飞时,我们表达的意思是一般的鸟都会飞,并不是表达所有的鸟都会飞。我们还应该承认一个事实:有些鸟不会飞。这样可以塑造一下继承关系。

class Bird
{
    .....            // 没有声明fly函数
};
class FlyingBird:public Bird
{    
    public:
        virtual void fly();
        .....
};
class Penguin:public Bird  // 企鹅是一种鸟
{
    .....    // 没有声明fly函数
};

2、请记住

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

二、避免遮掩继承而来的名称

1、继承而来的名称有什么风险

  derived classes内的名称会遮掩base classes内的名称,即使base classes 和 derived classes 内的函数有不同的参数类型也适用,而且不论函数式virtual 或 non-virtual一体适用。在public继承下从来没有人希望如此。考虑如下代码:

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();        // 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 并加上重载函数,而你又希望重新定义或覆写其中一部分,那么你必须为那些原本会被遮掩的每个名称引入一个using声明式,否则某些你希望继承的名称会被遮掩(using声明式会令继承而来的某给定名称之所有同名函数在derived class 中都可见。)。如下修改上述代码:

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的所有东西
        using Base::mf3;        // 在Derived 作用域内都可见        
        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

2、请记住

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

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


三、区分接口继承和实现继承

1、虚函数

  在基类中声明为vitual,并在一个或者多个派生类中被重新定义的函数。虚函数用于提供一类操作的统一命名。一般声明为虚函数是为了实现多态:使用基类指针指向一个派生类对象,通过这个指针调用基类和派生类的同名函数的时候,调用的是派生类中的方法(前提vitual函数肯定实现了)。
【Note】:
  虚函数:继承类必须含有的接口,可以自己实现,也可以不实现,而采用基类的实现。

2、纯虚函数

  被声明为纯虚函数的类一定是作为基类来使用的,含有纯虚函数的类被称为抽象类,抽象类不能实例化对象。因此纯虚函数一般用来声明接口其派生类必须实现这个函数。纯虚函数在基类中可以有函数实现,也可以没有。声明纯虚函数的原因是,在基类往往不合适进行实例化,比如一个shape类,中的draw方法。必须为纯虚函数,因为他不是任何一种形状。纯虚函数在基类中是没有定义的,必须在子类中加以实现。
【Note】:
  纯虚函数:要求派生类必须实现的函数,在基类中实现没有什么具体的意义。

3、重载函数

  重载函数在类型和参数数量上一定不相同,而重定义的虚函数则要求参数的类型和个数、函数返回类型相同;虚函数必须是类的成员函数,重载的函数则不一定是这样构造函数可以重载,但不能是虚函数,析构函数可以是虚函数。

  函数重载是在程序编译阶段确定操作的对象的,属于静态联编。虚函数是在程序运行阶段确定操作对象的,属于动态联编。重载的函数必须具有相同的函数名,函数类型可以相同也可以不同,但函数的参数个数和参数类型二者中至少有一个不同,否则在编译时无法区分。而虚函数则要求同一类族中的所有虚函数的函数名,函数类型,函数的参数个数和参数类型都全部相同,否则就不是重定义了,也就不是虚函数了。

4、接口继承和实现继承

  • 接口继承:派生类只继承函数接口,也就是声明。

  • 实现继承:派生类同时继承函数的接口和实现。

5、请记住

  • 成员函数的接口总是会被继承,public继承意味is-a。

  • 声明一个pure virtual 函数的目的是为了让derived classes 只继承函数接口。令人意外的是,C++竟然允许我们为pure virtual函数提供定义,但调用它的唯一途径是“调用时明确指出其class名称”。(baseObj>BaseName::virtualFuncName();)

  • 声明简朴的(非纯)impure virtual函数的目的,是让derived classes继承该函数的接口和缺省实现。但是,允许impure virtual函数同时指定函数声明和函数缺省行为,却有可能造成危险。

  • 声明non-virtual函数的目的是为了令derived classes继承函数的接口及一份强制性实现


四、考虑virtual函数以外的选择

1、non-virtual interface:提供非虚接口

class Object{
public:
    void Interface(){
        ···
        doInterface();
        ···
    }
private/protected:
    virtual doInterface(){}
}

  令客户通过public non-virtual成员函数间接调用private virtual函数,称为non-virtual interface(NVI)手法。

2、使用std::fun提供回调函数

class GameCharacter;   // 前置声明
// 以下函数是计算健康指数的缺省算法
int defaultHealthCalc(const GameCharacter& gc);
class GameCharacter
{
    public:
        typedef std::function<int (const GameCharacter&)> HealthCalcFunc;
        explicit GameCharacter(HealthCalcFunc hcf = defaultHealthCalc)
            : healthFunc(hcf)
        {}

        int healthValue() const
        { return healthFunc(*this); }
        ...
    private:
        HealthCalcFunc healthFunc;
};

3、请记住

  • 使用non-virtual interface(NVI)手法,那是Template Method设计模式的一种特殊形式。它以public non-virtual成员函数包裹较低访问性的virtual函数。

  • 以std::function成员变量替换virtual函数,因而允许使用任何可调用物搭配一个兼容于需求的签名式。这也是Strategy设计模式的某种形式。


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

class B
{
    public:
        void mf();
        ....
};
class D : public B
{
    public:
        void mf();     // 遮掩B::mf,条款33
        .....
};

D x;    // D 对象
B* pB = &x;
D* pD = &x;
// 针对D对象x,下面语句竟然调用的是不同的mf
pB->mf();      // 调用B::mf
pD->mf();      // 调用D::mf

  当mf(base class 内为 non-virtual 并在derived class 内被重载的函数)被调用,如何一个D对象都可能表现出B或D的行为;决定因素不在对象自身,而在于“指向该对象之指针”当初的声明类型。

1、请记住

  • 绝对不要重新定义继承而来的non-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 继承缺省参数值。
        // 但若以指针(或reference)调用此函数,可以不指定参数值。
        // 因为动态绑定下这个函数会从其base 继承缺省参数值。
        ... 
};
// 考虑下面指针
Shape* ps;                                        // 静态类型为Shape*, 没有动态类型, 尚未指向任何对象
Shape* pc = new Circle;                    // 静态类型为Shape*, 动态类型为Circle*
Shape* pr = new Rectangle;            // 静态类型为Shape*, 动态类型为Rectangle*

// 动态类型一如其名称所示,可在程序执行过程中改变(通常经由赋值完成)
ps = pc;        // ps 的动态类型如今是Circle*
ps = pr;            // ps 的动态类型如今是Rectangle*

  对象的所谓“静态类型”是指它在程序中被声明时所采用的类型(初始)。对象的所谓“动态类型”是指“目前所指对象的类型”(当前)。

1、请记住

  • 绝对不要重新定义一个继承而来的缺省参数值,因为缺省参数值都是静态绑定的,而virtual函数——你唯一应该覆写的东西——却是动态绑定。

七、通过复合塑模出has-a或者"根据某物实现出"

  复合(组合)(composition)是类型之间的一种关系,当某种类型的对象内含它种类型的对象,便是这种关系。条款32曾说,“public继承”带有is-a(是一种)的意义。复合也有它自己的意义。实际上它有两个意义。复合意味has-a(有一个,包含关系)或 is-implemented-in-terms-of(根据某物(底层)实现出(适配器),类似STL的容器适配器如Stack,Queue等)。当复合发生于应用域内的对象之间,表现出has-a的关系;当它发生于实现域内则是表现is-implemented-in-terms-of的关系。

1、请记住

  • 复合的意义和public继承完全不同。

  • 在应用域,复合意味has-a(有一个)。在实现域,复合意味is-implemented-in-terms-of(根据某物实现出)。


八、明智而审慎地使用private继承

1、private继承有什么问题

class Person{……};
class Student: private Person{……};
void eat(const Person& p);
void study(const Student& s);

Person p;
Student s;

eat(p);
eat(s); // 出错,因为private继承不是is-a关系

  private继承意味is-implemented-in-terms-of(根据某物实现出)。private继承纯粹只是一种实现技术(这就是为什么继承自一个private base class 的每样东西在你的class 内都是private:因为它们都只是实现枝节而已)。

  如果D 以private 形式继承 B,意思是 D 对象根据 B 对象实现而得,再没有其他意涵了。

2、请记住

  • Private 继承意味 is-implemented-in-terms-of(根据某物实现出),它通常比复合的级别低(尽量使用组合)。但是当derived class需要访问 protected base class 的成员,或需要重新定义继承而来的virtual函数时,这么设计是合理的。即便如此,一个混合了public继承和复合的设计,也能达成你所要的行为。

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


九、明智而审慎地使用多重继承

1、菱形继承

在这里插入图片描述

//包含A对象
class A{
    
};
//包含A,B对象
class B:public A{
    
};
//包含A,C对象
class C:public A{
    
};
//包含A,A,B,C,D对象
class D:public B, public C{
    
}

  由于菱形继承,基类被构造了两次。其实,C++也提供了针对菱形继承的解决方案的:

//包含A对象
class A{
    
};
//包含A,B对象
class B:virtual public A{
    
};
//包含A,C对象
class C:virtual public A{
    
};
//包含A,B,C,D对象
class D:public B, public C{
    
}

  使用虚继承,B,C对象里面会产生一个指针指向唯一一份A对象

  virtual 继承所需要付出的成本是巨大的:首先,使用virtual 继承的那些classes 所产生的对象往往比使用non-virtual 继承的兄弟们的体积大,访问virtual base classes 的成员变量时,也比访问non-virtual base classes的成员变量速度慢。其次,virtual base 的初始化责任是由继承体系中的最低层 class 负责,这暗示:(1)classes 若派生自virtual bases而需要初始化,必须认知其virtual bases——不论那些bases 距离多远,(2)当一个新的derived class加入继承体系中,它必须承担其virtual bases(不论直接或间接)的初始化责任。

2、请记住

  • 多重继承比单一继承复杂。它可能导致新的歧义性,以及对virtual继承(虚基类)的需要。

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

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

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

~青萍之末~

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值