C++面向对象基础——继承

继承

面向对象程序设计中最重要的一个概念是继承。继承允许我们依据另一个类来定义一个类,这使得创建和维护一个应用程序变得更容易。这样做,也达到了重用代码功能和提高执行效率的效果。

当创建一个类时,您不需要重新编写新的数据成员和成员函数,只需指定新建的类继承了一个已有的类的成员即可。这个已有的类称为基类,新建的类称为派生类

继承的权限和作用域

C++中的类也是可以继承的,用冒号标识继承的类,并且还可以表时是以什么权限(public,private,protected)去继承的。

  • 公有继承:继承父类的 public 和 protected 的所有方法和成员,保持权限不变。
  • 保护继承:继承父类的 public 和 protected 的所有方法和成员,权限全部改为 protected
  • 私有继承:继承父类的 public 和 protected 的所有方法和成员,权限全部改为 private

继承之后,子类可以使用父类的所有方法和成员。在不指定命名空间的时候,优先访问本类命名空间的方法和成员,否则逐级向父类寻找,第一个找到的会被调用。

我们也可以在成员名或者方法名前指定作用域指定访问哪个父类的成员或者方法。

在构造一个类的时候,会根据继承顺序,从基类开始构造,然后依次调用到最终类的构造函数,顺序是从基类到派生类;析构函数刚好相反,它会从派生类开始调用析构函数,然后逐步调用到基类的析构函数。

如果成员为一个类,那么先调用成员的构造,再调用自己的构造。

当子类重定义了与父类重名的函数,父类所有同名重载的函数都会失效,我们需要通过指定命名空间去访问指定类的函数。

继承方式

有三种继承方式

  • 单继承:只有一个父类
  • 多继承:有多个父类,调用构造函数的时候父类构造函数从左到右调用
  • 菱形继承:有多个父类,但是多个父类可能又继承自同一基类

在多继承或者是菱形继承中,我们若要访问父类方法或者是成员,可能需要指定作用域才能成功,否则会提示方法或成员不明确。为了解决这个问题,我们可以使用虚继承,让继承同一个父类的类虚继承父类,在数据修饰方法后加一个 virtual 表示虚继承。

虚继承

如果一个类从多个类继承而来,而这些类有公共基类。那么在多该基类中定义的成员访问时会出现二义性。C++设计虚继承来解决这个问题。虚继承的本质就是子类引用父类的内存空间,而不创建自己的内存空间。这样既解决了多重继承可能引发的二义性问题,也使得内存得以释放。

在虚继承过程中的基类被叫做:虚基类

那么实际上,虚继承的本质就是使用一个指向虚基类的指针,这样就无论你怎么继承,就只有一份基类内存空间。

C++使用关键字virtual来进行虚继承。

多态

虚函数

虚函数 是在基类中使用关键字 virtual 声明的函数。在派生类中重新定义基类中定义的虚函数时,会告诉编译器不要静态链接到该函数。

我们想要的是在程序中任意点可以根据所调用的对象类型来选择调用的函数,这种操作被称为动态链接,或后期绑定

纯虚函数

纯虚函数就是不能被实现的函数,它只定义了一个标准,要求继承它的这个类必须实现该函数。定义纯虚函数是为了实现一个接口,起到一个规范的作用,规范继承这个类的程序员必须实现这个函数,通常用来写扩展接口。

纯虚函数是在基类中声明的虚函数,它在基类中没有定义,但要求任何派生类都要定义自己的实现方法。在基类中实现纯虚函数的方法是在函数原型后加 =0

具有纯虚函数的类会被视为抽象类,抽象类不能实例化,派生类必须实现纯虚函数,如果派生类没有实现纯虚函数的定义,那么该派生类也会成为抽象类。

虚表

当对象的成员函数有虚函数的时候,对象的首 4/8 个字节会保存虚表的地址。我们跟一下看看:

然后我们跟一下这个虚表的地址,跟这个地址之后,发现来到了一个跳转表

于是我们就找到了 eat 函数。

这个结构是这样的:

对象的头四个字节是 虚函数表 的指针,因为对象如果继承多个父类则有可能出现多个虚函数表,因此这个地方只存一个指针,指向一个存了虚函数表地址的数组结构。然后再由这个结构找到真正的虚函数表,虚函数表项就是这样 jmp 某个函数的形态。

虚析构

因为多态的缘故,我们可以建立一个 基类 指针指向一个派生类,但是当我们 delete 这个指针的时候,程序并不知道它这个对象可能是派生类,因此在 delete 这个对象的时候可能会跳过执行派生类的虚构函数并出现一些问题。

实际上,派生类的析构函数会自动调用基类的析构函数。只要基类的析构函数是虚函数,那么派生类的析构函数不论是否用virtual关键字声明,都自动成为虚析构函数。一般来说,一个类如果定义了虚函数,则最好将析构函数也定义成虚函数。析构函数可以是虚函数,但是构造函数不能是虚函数。