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

目录

一、确保public继承塑模出is-a关系

二、避免隐藏继承而来的名称

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

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

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

六、不要重新定义继承而来的缺省参数

七、尽量使用复合塑模出has-a

总结


一、确保public继承塑模出is-a关系

还是先以一个简单的例子来说明

class Person
{
    //…………
};

class Student : public Person
{
    //…………
};

根据生活经验我们知道,每一个学生都是人,但是并不是每一个人是学生,这就是所谓的is-a的关系。我们可以预期,对人可以成立的每一件事对学生也是成立的,学生是一种特殊的人。

所以,任何函数如果参数是Person类型(或者是Person类型的指针或者引用),那么也可以将一个student类型传过去。

这个观点只对public继承才会成立,private继承并不属于is-a的关系。

public继承和is-a的等价关系听起来十分的简单,但是十分容易犯错误。

再举一个简单的例子:

企鹅是鸟,符合is-a的关系

鸟会飞,这也是一个事实

按照我们之前所说,企鹅是鸟,但是企鹅不会飞,如果按照上述的继承体系,我们就要好好研究一下,怎么处理企鹅会飞这一问题

有两种做法:

1、令程序在运行期间发生错误

2、在编译期间直接报错,"企鹅不会飞"

我们一般采用的是第二种做法,我们应该防止无效代码通过编译

二、避免隐藏继承而来的名称

我们还要回顾一下C++的相关概念

 所谓的隐藏是指隐藏名称,至于函数的参数和返回值并不重要

接下来再回顾一下作用域

派生类的作用域是内嵌于基类的作用域中

class Base
{
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();  // 没问题调用Derived::mf1
d.mf1(x); // 有问题Derived::mf1隐藏了Base::mf1
d.mf2();  // 没有问题调用Base::mf2
d.mf3();  // 没有问题调用Derived::mf3
d.mf3(x); // 有问题Derived::mf3隐藏了Base::mf3

 如果想要调用到Base类中的成员函数,需要我们手动指明作用域

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

身为class的设计者,有时希望派生类只继承成员函数的接口,有时又希望派生类同时继承函数的接口和实现,但是又希望能够重写它们所继承的实现,有时候又希望派生类同时继承函数的接口和实现,并且不允许重写任何东西

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
{

};

我们看这个简单的继承关系

Shape是一个抽象类,因为它有纯虚函数draw,所以它不能够实例化出对象,它只能够实例化它的派生类,但是他还是强烈的影响了与它继承的所有派生类,因为它的全部派生类都继承了它的纯虚函数,如果派生类没有重写该虚函数,导致它的派生类依然是抽象类。

1、成员函数的接口总是被继承的,所谓接口就是值它的函数声明

2、纯虚函数只具体指定接口继承

3、非纯虚函数具体指定接口继承及缺省实现继承

4、普通成员函数具体指定接口继承以及强制性实现继承

但是普通成员函数同时指定函数声明和函数缺省行为,却可能造成危险

假如有一家航空公司,它现在有两种飞机A,B,两者都以相同的方式飞行

下面是简单的代码实现

class AirPlane
{
public:
    virtual void fly(const std::string &destination);
};

void AirPlane::fly(const std::string &destination)
{
    std::cout << " fly "
              << "青岛"
              << " to " << destination << std::endl;
}

class ModelA : public AirPlane
{
public:
    virtual void fly(const std::string &destination)
    {
        AirPlane::fly(destination);
    }
};

class ModelB : public AirPlane
{
public:
    virtual void fly(const std::string &destination)
    {
        AirPlane::fly(destination);
    }
};

为了表明不同飞机的飞行方式,所以fly函数加上了virtual声明,代表继承接口和缺省实现继承

这是一个典型的面向对象的设计,两个class共享同一份fly函数,所有的飞机共性都放到了Base类中,避免了代码重复,减少长期维护的成本。

现在该公司添加了C型飞机,该公司程序员增加了class ModeC,但是忘记重新定义了C的fly函数

class ModelC : public AirPlane
{
public:
    //…………
};

这将造成重大灾难,因为C机型飞行方式根本与前两种飞行方式完全不同

为了避免这样的问题,我们可以将fly函数声明为纯虚函数,只继承它的接口,不继承它的实现,这样因为没有重写虚函数导致C还是抽象类而报错

这里将基类改成抽象类 

class AirPlane
{
public:
    virtual void fly(const std::string &destination) = 0;
};

编译时就会报错,避免了无效代码编译通过

 


class ModelC : public AirPlane
{
public:
    virtual void fly(const std::string &destination)
    {
        std::cout << "Model C "
                  << " fly "
                  << " 青岛 "
                  << "to" << destination << std::endl;
    }
};

这里我们重写C机型的fly函数,而其他机型的fly函数什么都不用做


class AirPlane
{
public:
    virtual void fly(const std::string &destination) = 0;
};

void AirPlane::fly(const std::string &destination)
{
    std::cout << " fly "
              << "青岛"
              << " to " << destination << std::endl;
}

class ModelA : public AirPlane
{
public:
    virtual void fly(const std::string &destination)
    {
        AirPlane::fly(destination);
    }
};

class ModelB : public AirPlane
{
public:
    virtual void fly(const std::string &destination)
    {
        AirPlane::fly(destination);
    }
};

class ModelC : public AirPlane
{
public:
    virtual void fly(const std::string &destination)
    {
        std::cout << "Model C "
                  << " fly "
                  << " 青岛 "
                  << "to" << destination << std::endl;
    }
};

 

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

假如你在写一个FPS游戏,游戏中的人物有血量限制,需要为任务角色类添加一个成员函数来计算血量。因为每一个人物会有不同的方式计算血量,所以让这个成员函数声明为virtual

class Character
{
public:
    virtual int healthValue() const;
    //…………
};

但是我们还可以选择更好的实现方法

class Character
{
public:
    int healthValue() const
    {
        int retVal = 0;
        //…………

        retVal = doHealthValue();
        //…………

        return retVal;
    }
private:
    virtual int doHealthValue() const
    {
        //………………
    }
};

这种通过非virtual成员函数间接调用private virtual函数称为"non-virtual interface"(NVI)手法

它是Template Method设计模式的一种独特表现形式,这个非virtual函数称为外覆器。

外覆器能够确保在一个virtual函数被调用之前设定好适当场景,并在调用结束之后清理场景

事前工作包括锁定互斥器,记录日志等等


NVI模式下,没有必要让virtual函数一定是private的,可以灵活变通,某些class继承体系要求派生类在virtual函数的实现内必须调用其基类的兄弟,为了让这种操作合法,virtual函数必须声明为protected


另一种做法是使用function来完成Strategy模式

我们可以让每个角色的构造函数接受一个function,function中是计算血量的函数(仿函数,lambda表达式),然后我们直接调用function就能够根据不同的角色来计算血量


int defaultHealthValue(const Character& c);

class Character
{
    typedef std::function<int(const Character&)> HealthCalcFunc;
public:
    Character(HealthCalcFunc hfc = defaultHealthValue)
        :_healthFunc(hfc)
    {}

    int healthValue() const
    {
        return _healthFunc(*this);
    }

private:
    HealthCalcFunc _healthFunc; 
};

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

class A
{
public:
    void func() const
    {
        std::cout << "Hello A" << std::endl;
    }
};

class B : public A
{
};

B b;
A *pa = &b;
pa->func();//调用A::func

B* pb = &b;
pb->func();//调用B::func

原因是普通成员函数是静态绑定的,pa定义为A*类型,所以通过它调用的普通成员函数永远是指向A所定义的版本

virtual成员函数是动态绑定的,如果func是virtual函数,无论是通过pa调用func还是通过pb调用func,他都会调用B::func

简单说:

先看函数类型,如果是普通成员函数那么就看指针类型,如果是virtual函数那么就看指针实际所指的空间类型

为了避免出现上面的情况,在任何情况下都不应该重新定义一个继承而来的普通成员函数

六、不要重新定义继承而来的缺省参数

根据前面所说,不要重新定义继承而来的普通成员函数

所以本条成立的前提是:继承一个带有缺省值的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
    {

    }
};

这样写出的代码十分的诡异

当以对象调用draw函数时,一定要传参数

因为静态绑定下这个函数并没有从基类继承缺省值

如果以指针或者引用去调用draw,可以不指定参数

因为动态绑定下这个函数会从基类继承缺省参数

解决办法还是NVI手法

基类内的一个普通成员函数调用private virtual函数

class Shape
{
public:
    enum ShapeColor 
    {
        RED,
        GREEN,
        BLUE
    };

    void draw(ShapeColor color = RED) const
    {
        doDraw(color);
    }
private:
    virtual void doDraw(ShapeColor color) const = 0;
};

class Rectangle : public Shape
{
public:
    void draw(ShapeColor color = GREEN) const
    {
        doDraw(color);
    }
private:
    virtual void doDraw(ShapeColor color) const
    {
        //…………
    }
};

七、尽量使用复合塑模出has-a

public 继承是一种 is-a 的关系。也就是说每个派生类对象都是一个基类对象。
组合是一种 has-a 的关系。假设 B 组合了 A ,每个 B 对象中都有一个 A 对象。
  
继承允许你根据基类的实现来定义派生类的实现。这种通过生成派生类的复用通常被称为白箱复用(white-box reuse) 。术语 白箱 是相对可视性而言:在继承方式中,基类的内部细节对子类可见 。继承一定程度破坏了基类的封装,基类的改变,对派生类有很大的影响。派生类和基类间的依赖关系很强,耦合度高。
对象组合是类继承之外的另一种复用选择。新的更复杂的功能可以通过组装或组合对象来获得。对象组合要求被组合的对象具有良好定义的接口。这种复用风格被称为黑箱复用(black-box reuse) ,因为对象的内部细节是不可见的。对象只以 黑箱 的形式出现。
组合类之间没有很强的依赖关系,耦合度低。优先使用对象组合有助于你保持每个类被
封装。
实际尽量多去用组合。组合的耦合度低,代码维护性好。不过继承也有用武之地的,有些关系就适合继承那就用继承,另外要实现多态,也必须要继承。类之间的关系可以用继承,可以用组合,就用组合。


总结


以上就是今天要讲的内容,本文仅仅回顾了C++继承和面向对象的细节

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值