本期我们学习 C++ 面向对象编程中的继承。
面向对象编程是一个巨大的编程范式,类之间的继承是它的一个基本面,它是我们可以实际利用的最强大的特性之一。
先了解这些
继承允许我们有一个相互关联的类的层次结构。展开来说,它允许我们有一个包含公共功能的基类,我们可以从基类中分离出来,基于父类(基类)中创建子类。
类继承的主要作用是它可以帮助我们避免代码重复。
使用类继承后,我们就不需要一遍又一遍的重复一些代码了,我们可以把类之间的所有公共功能放在一个父类中。然后从基类(父类)创建(派生)一些类,稍微改变下功能或者引入全新的功能。这样我们就不用像写模板那样不断重复了。
我们来看看如何在源代码中定义它。
上面的代码中我们创建了一个 Entity 类,它用来描述游戏中的所有实体对象。
在我们游戏中我们可能有很多非常具体的实体,然而在某些方面,它们的功能是一样的。
例如也许每个实体在我们的游戏中都有自己的位置,这可以通过两个 float 的变量来表达。在上面的代码中,我们有设置了 float X和 float Y。
我们可能想赋予每个实体移动的能力,这可以通过 Move 方法实现,它有 xa 和 ya 两个参数作为我们坐标数据的偏移量。
好了,现在我们有了一个基类,Entity 类。
我们说过在游戏中创建的每一个实体都将是具有这些特征的。
我们继续创建一个新类型的实体。
在上面的代码中,我们创建了一个 Player 类,目前还没有用到所谓的继承概念。我们是从零开始的,这个 Player 类也有位置,因为这也是一个实体,它需要位置信息。
我们还想让它能够移动,所以我们定义 Move 函数,你可以看到当前我们完成的东西和上面的 Entity 类很像。
接下来这个 Player 类有我们想要存储的额外数据。例如姓名 Name 或打印姓名的方法 PrintName,我们加上了这些内容。
到这里,你可以看到它们实际上已经是不同的类了。
然而能看到有相当多的代码只是被复制粘贴。为了改变这种状况,我们能做的就是利用继承。
继承
我们可以扩展 Entity 实体类来创建 Player 类,然后让它存储新数据,比如姓名 Name 或打印姓名的方法 PrintName。
现在让我们把 Player 变成 Entity 的子类。
这需要我们在类声明后写一个冒号,然后写上 public Entity。
现在在我们写的代码中,发生了一些事情:Player 类现在不仅拥有 Player 类型,它也有 Entity 类型,它现在是两种类型了。
类型在 C++ 中是相当复杂的主题。一方面它们实际上并不存在,然而另一方面,它们又会在很多地方制造麻烦,后面的系列中我们再去深入了解它是如何工作的。
Player 现在拥有 Entity 拥有的所有东西,比如拥有类成员 X 和 Y,比如 Move 方法。因为它们本身也在 Player 中,所以现在我们把 Player 类中和 Entity 所有重复的代码都删掉,留下一些不一样的东西就可以了,这些是我们的附加功能。
现在这个 Player 类看起来很干净。
我要提醒你们,它实际上也是一个 Entity,这意味着仅仅看这个类的内容并不能告诉我们关于它的所有信息,如果想全面了解的话,我们必须去找 Entity,看看它有什么。因为就 Player 而言,任何在 Entity 类中不是私有的东西实际上都可以被 Player 访问。
验证
让我们来测试一下。
在上面的代码中,我们创建一个 Player 类的实例 player。它不仅可以调用 printName 函数,——这个函数本身就是在 Player 类里面的。因为现在还没有名字的具体内容,所以直接运行会出问题。而且也可以调用 Move 函数,并且可以访问 X,就好像它就是一个 Entity 一样,因为它继承了 Entity 所有的功能。
其实在这里可以应用的一个概念是 多态。这个内容将在以后的内容中深入探讨。简单来说,多态是一个单一类型但是有多个类型的意思。
还记得我提到的 Player 不只是 Player类型,而且也是一个 Entity类型。这意味着我们可以在任何我们想要使用 Entity 的地方使用 Player。因为 Player 拥有 Entity 所拥有的一切,再加上多一点点东西,它甚至不需要有更多那一点点东西,它们两个也可能是完全一样的。——Player 总是 Entity 的超集,这意味着 Player 总是会拥有 Entity 所拥有的一切,也可能更多一点。
举个例子。
如果我想创建一个打印 Entity 对象的函数,例如访问 X 和 Y 变量,并让它们打印到控制台,我可以传入 Player 对象到相同的函数中,即便这个函数原本是接受 Entity 类型的参数。
可以这样操作的原因是,Player类型必定会有这些 X 和 Y 变量。作为 Entity 类的子类,它包含了 Entity 所有的东西。我们还可以做很多类似的事情。
我们也可以改变父类或基类的行为。例如通过重写一个方法,并给它新的代码来代替它(父类方法)运行。
但本期的主要内容是继承,它是我们一直在使用的一种方式,是我们扩展现有类并为基类提供新功能的一种方式,这是面向对象编程最重要的东西之一。
记住当你创建一个子类时,它将包含它父类所含的一切。
我还有一个证明这个的方法。
另一种验证方法
记得我们基类中有浮点数 X 和 Y 。
我们通过使用 sizeof 函数计算 Entity 的对象的大小并打印到控制台。
你可以看到输出的它的大小是8。没问题,因为 Entity类中有两个浮点数 XY。
现在让我们继续打印 Player 的大小。
假设我们删除继承的代码,那么 Player 只是一个独立的类,只有这个 const char指针,在32位的应用程序中,这应该是 4 字节的内存。
然而既然我们扩展了Entity,这意味着它继承了 Entity 类中的所有变量,所以实际上应该是4+4+4 = 12。
运行下面的程序看一下对不对。
的确如此。
你可以看到我们实际上继承了 Entity 拥有的所有东西。好像我们复制粘贴了所有东西。
请记住,这些类的大小和内存实际上是可以变化的。
那么,除了这个区别还有其他的变化吗?
有的,如果我们开始重写函数和 Player 类,那么我们就需要维护一个V表(虚函数表),也就是要占用额外的内存。这个先简单了解一下,本期先不讲这个。
以上就是继承在 C++ 类中的如何工作的要点。
最后的话
在本系列课程中我们将分解所有与面向对象编程相关的独立概念,多态、继承等等。所有这些概念都值得好好研究。如果将整个体系如果比喻成一个物件,我还只是接触到了它的表面而已。
好了,本期的内容就是这些,下期再见。