C++面向对象继承与多态

前言

经过前两篇博文,我已经将多态的前提条件总结得七七八八了。这一篇开始正式展开讲多态,以及咱们为何要使用多态。编程

多态

什么是多态

引用百度百科的定义:设计模式

多态(Polymorphism)按字面的意思就是“多种状态”。在面向对象语言中,接口的多种不一样的实现方式即为多态。引用Charlie Calverts对多态的描述——多态性是容许你将父对象设置成为一个或更多的他的子对象相等的技术,赋值以后,父对象就能够根据当前赋值给它的子对象的特性以不一样的方式运做(摘自“Delphi4 编程技术内幕”)。简单的说,就是一句话:容许将子类类型的指针赋值给父类类型的指针。

个人理解是:子类能够经过父类的指针或者引用,调用子类重写父类的虚函数,以达到一个类型多种状态的效果。函数

这听起来好像没有什么,我能够直接经过子类的对象调用成员函数不就好了,为啥还要舍近求远将其赋值到一个父类指针再调用呢?起初学习的时候我也不懂为何,直到后来我遇到了一个很典型的例子才恍然大悟,这个例子我会在下面讲到。学习

多态的条件

前面也零零散散地介绍了C++多态的条件,这里总结一下:this

  • 须要有继承
  • 须要使用父类的指针或引用
  • 父类须要有虚函数,子类要重写父类的虚函数

须要上转型是Java多态的条件,C++主要是经过使用父类的指针或者引用来实现的,也能够认为是一种上转型吧。正是由于使用了父类的指针或者引用,才使得他可以调用子类的虚函数,而不是像上一篇的上转型致使的静态绑定,最终调用的是父类的虚函数。咱们经过如下代码来回顾一下:spa

class base {
public:
    virtual void do_something() //有虚函数 {
        cout << "I'm base class" << endl;
    }
};

class derived : public base            //有继承
{
public:
    void do_something() //子类重写了父类的虚函数 {
        cout << "I'm derived class" << endl;
    }
};

void fun1(base &b) //父类的引用 {
    b.do_something();
}

void fun2(base *b) //父类的指针 {
    b->do_something();
}

void fun3(base b) {
    b.do_something();
}

int main() {
    derived d;
    fun1(d);    //I'm derived class
    fun2(&d);    //I'm derived class
    fun3(d);    //I'm base class
    return 0;
}

fun1()和fun2()实现的过程都是动态绑定的,即运行时才动态肯定要调用哪一个函数。那他到底是怎么实现的?设计

动态绑定的原理

你们还记得虚类的对象是有一个vptr,多个同类对象的vptr指向同一个vtable。动态绑定就是经过这个vptr间接寻址来实现的。虽然子类对象被赋值到了父类的指针,可是对象的vptr是没有改变的,他指向的仍是子类的vtable。 因此父类指针去调用某个虚函数的时候,就会去vtable里面找函数入口,那找到的天然是子类的函数入口。因此他不是在编译期间就肯定的,而是在代码运行到那一行的时候才找到的函数入口。指针

那为何只有指针或者引用才能达到这个效果呢?《深度探索C++对象模型》这本书对此有这样一个解释:code

一个pointer或一个reference之因此支持多态,是由于它们并不引起内存任何与类型有关的内存委托操做。会受到改变的,只有它们所指向内存的大小和解释方式而已。

这样读起来有点拗口,简单讲就是指针或者引用的赋值并不会改变原对象内存里的内容,他只会改变对内存大小及内容的解释方式。举个简单的例子:我将int变量的地址赋值给了char型指针,char型指针才无论原来的变量是什么,他对外只解释一个字节的内容。对象

简单的内存模型
同理可知,子类对象的内存内容并无发生改变,那么对象的vptr仍是指向子类的vtable,因此调用的仍是子类的的成员函数。而简单的上转型并不会有这样的效果,他会对内存进行从新分配。

另外说一下,只用C++有静态绑定这个概念,其余面向对象类的语言都是动态绑定。能够看出C语言的知识是很细致入微的。

为何要使用多态

到此其实多态已经讲完了,铺垫了这么多前置知识,其实多态就这么一点点。我主要仍是想讲讲为何要使用多态,只有知道了为何,才能使咱们在设计代码的时候考虑获得如何运用这个知识点。咱们用一个游戏的例子来讲明为何。

游戏的描述以下:

  • 游戏有一个英雄角色,角色属性有生命(hp)和攻击力(ack)
  • 英雄能够对怪物进行攻击,同时也会受到怪物的攻击
  • 怪物属性有生命(hp)和攻击力(ack)
  • 怪物能够对英雄进行攻击,也会受到英雄的攻击
  • 现阶段有三种怪物:狼人,僵尸,女巫

咱们先来实现怪物类:

class wolf //狼人类 {
public:
    wolf(int hp, int ack)
    : hp(hp)
    , ack(ack)
    {}

    bool damage(int dm) {
        if (this->hp <= 0) return false;
        this->hp -= dm;
        return false;
    }

    bool attack(hero &hr) {
        return hr.damage(this->ack);
    }

private:
    int hp;
    int ack;
};

class zombie {
public:
    zombie(int hp, int ack)
    : hp(hp)
    , ack(ack)
    {}

    bool damage(int dm) {
        if (this->hp <= 0) return false;
        this->hp -= dm;
        return false;
    }

    bool attack(hero &hr) {
        return hr.damage(this->ack);
    }

private:
    int hp;
    int ack;
};

class witch {
public:
    witch(int hp, int ack)
    : hp(hp)
    , ack(ack)
    {}

    bool damage(int dm) {
        if (this->hp <= 0) return false;
        this->hp -= dm;
        return false;
    }

    bool attack(hero &hr) {
        return hr.damage(this->ack);
    }

private:
    int hp;
    int ack;
};

而后咱们来实现英雄类:

class hero {
public:
    hero(int hp, int ack)
    : hp(hp)
    , ack(ack)
    {}

    bool damage(int dm) {
        if (this->hp <= 0) return false;
        this->hp -= dm;
    }

    bool attack(wolf &wf) {
        return wf.damage(this->ack);
    }

    bool attack(zombie &zb) {
        return zb.damage(this->ack);
    }

    bool attack(witch &wt) {
        return wt.damage(this->ack);
    }

private:
    int hp;
    int ack;
};

咱们发现,一样逻辑的attack()函数,咱们须要实现三次。若是后期游戏要增添新的怪物,咱们还得继续写attack()函数。 这其实仍是一种面向过程的思想,并非说写几个类出来就是面向对象了。并且这也彻底不符合咱们程序猿的编程习惯,咱们程序猿不喜欢重复的东西。欸,这个时候多态就能发挥他的做用了。

咱们来定义怪物们的基类:

class monster {
public:
    virtual bool damage(int dm) = 0;
    virtual bool attack(hero &hr) = 0;
};

以前说了,咱们并不关心这个基类的虚函数具体是怎么实现的,那么咱们就能够将其声明为纯虚类。而后让怪物都继承这个基类,实现上面这两个函数就能够了。这样咱们就能够将hero类改形成这样:

class hero {
public:
    hero(int hp, int ack)
    : hp(hp)
    , ack(ack)
    {}

    bool damage(int dm) {
        if (this->hp <= 0) return false;
        this->hp -= dm;
    }

    bool attack(monster &ms) //参数修改成monster类必定要用指针或者引用 {
        return ms.damage(this->ack);
    }

private:
    int hp;
    int ack;
};

这样代码是否是就简洁不少。并且根据多态的性质,不一样的怪物会调用其各自的damage()函数。之后要是新增怪物,只要继承和实现虚基类就行了,hero类并不须要进行修改。这就体现了面向对象编程的优点了,这还只是其中之一。

同理,要是有多种英雄,咱们一样能够抽象出一个英雄类的虚基类,而后派生出各式各样的英雄,怪物类也不须要重复写多个attack()函数。

有同窗仍是以为怪物类的实现仍是重复度过高了,这没有体现多态的优点啊。其实否则,前面说到每一个子类都应该重写基类的虚函数,是由于不一样的子类都应该有他的特别之处, 因此才叫派生嘛。若是子类和子类,或者子类和基类彻底同样那就没有必要继承与派生了。

这里重复度高只是由于代码量小,我只是举了个小小的例子,其实在真正的游戏中不一样怪物子类的attack()函数和damage()函数的内部细节应该是不同。好比不一样的怪物有不一样的攻击特效,有不一样的受击效果,有不一样的技能冷却时间等等。这些细节都是经过子类去重写基类的虚函数,才得以体现的。

总结

到此为止,我所了解的继承与多态算是总结完毕了。会简单地封装几个类并非面向对象编程,只有完全理解了封装、继承与多态,面向对象编程才算是入了个门。只有理解了这些,咱们才能开始学习设计模式,才能领悟到设计模式的精髓所在。学设计模式建议你们去看《大话设计模式》这本书,之后有时间我也会在个人博客里总结一些设计模式。

相关文章
相关标签/搜索