继承和派生、虚继承和虚基类、虚基类表和虚基类指针
继承和派生
继承概述
继承基本概念
c++最重要的特征是代码重用,通过继承机制可以利用已有的数据类型来定义新的数据类型,新的类不仅拥有旧类的成员,还拥有新定义的成员。
继承可以实现代码的复用,被继承的类称为父类或超类(Superclass),继承而得到的类称为子类或派生类
一个B类继承于A类,或称从类A派生类B。这样的话,类A成为基类(父类), 类B成为派生类(子类)。
派生类中的成员
-
派生类中的成员,包含两大部分:
-
一类是从基类继承过来的
-
从基类继承过过来的表现其共性
-
一类是自己增加的成员
-
新增的成员体现了其个性。
继承的内容
派生类继承基类,派生类拥有基类中全部成员变量和成员方法(除了构造和析构之外的成员方法)
基类的私有成员是会被派生类继承的,但是不能被派生类访问
从物理上讲是复制过来了,在内存中确实有复制。但是从程序上看是被屏蔽了,不能直接调用。
对于基类private类型的成员变量,无论是公有继承还是私有继承,在派生类中定义的成员函数都不能直接访问基类的私有成员,只能通过基类的public或protect成员函数访问基类的私有成员。
派生类定义
派生类定义格式:
- Class 派生类名 : 继承方式 基类名 { //派生类新增的数据成员和成员函数 }
继承方式:
- public : 公有继承
- private : 私有继承
- protected : 保护继承
派生类访问控制
派生类继承基类,派生类拥有基类中全部成员变量和成员方法(除了构造和析构之外的成员方法),但是在派生类中,继承的成员并不一定能直接访问,不同的继承方式会导致继承而来的属性拥有不同的访问权限。(根据继承方式缩小访问权限)
派生类中:虽然继承了所有属性和方法(除了构造和析构),但是基类的私有成员在派生类中无法访问(无论继承方式是什么),而非私有成员将会根据继承方式缩小访问权限(公有继承由于拥有最高权限所以基类访问权限不变,保护继承会将基类中公有权限缩小至保护权限,私有继承会将基类中公有权限和保护权限缩小至私有权限)
私有继承
私有继承后父类公有成员和保护成员都作为子类的私有成员,并且不能被这个子类的对象所访问。
如果子类再派生出一个孙子类的话,因为父类的成员在子类中只有公有成员和保护成员可以访问,并且属性降级为private,所以孙子类即使是公有继承子类的,也不能访问private成员。
所以在私有继承时,父类的成员只能由直接派生子类访问,而无法再往下继承。
对象构造和析构
继承时,子类并不会继承父类中的构造函数和析构函数,而是再子类构造中调用父类的构造
对象构造和析构的调用顺序
- 子类对象在创建时会首先调用父类的构造函数(父类构造先于子类构造)
- 析构函数调用顺序和构造函数相反,先调用子类中构造,再调用父类中构造
继承中的构造和析构的调用规则
- 如果子类没有显示调用父类含参数的构造函数,那么在子类实例化过程中,系统会自动调用父类默认构造(无参构造)
- 在子类构造中调用父类构造,不能再函数体中调用(这样会成为匿名函数),只能在初始化列表中调用父类构造
1 .在子类体中调用父类构造,变为匿名对象
2 .在初始化列表中调用父类构造
- 当父类构造函数有参数时(自定义有参构造后系统将不提供默认构造),需要在子类初始化列表(参数列表)中显示调用父类构造函数
系统会在子类构造中调用父类默认(无参)构造
父类没有默认构造,则需要进行显示调用
父类中使用自定义构造后,需要在自定义构造中显示调用(初始化列表)
调用子类构造前会先调用父类构造
子类对象在创建时会首先调用父类的构造函数(父类构造先于子类构造),父类构造函数执行完毕后,才会调用子类的构造函数
父类的构造函数,用于初始化父类中的成员
子类的构造函数,用于初始化子类新增或重写成员
父类构造函数执行完毕后,才会调用子类的构造函数
析构函数调用顺序和构造函数相反,先调用子类中构造,再调用父类中构造
继承中同名成员的处理方法
当子类成员和父类成员同名时,子类依然从父类继承同名成员,当访问时就近调用自身成员
如果子类有成员和父类同名,子类访问其成员默认访问子类的成员(本作用域,就近原则)
-
任何时候重新定义基类中的一个重载函数,在新类中所有的其他版本将被自动隐藏.
- 父类和子类中同名函数,子类会将父类中所有同名函数(无论是否为重载版本),只能通过作用域访问父类函数
在子类通过作用域::进行同名成员区分(在派生类中使用基类的同名成员,显示使用类名限定符)
: 在子类构造函数的函数体中指定作用域也无法调用父类构造,只会生成匿名对象
继承中的静态成员特性
静态成员函数和非静态成员函数的共同点:
- 他们都可以被继承到派生类中。
- 如果重新定义一个静态成员函数,所有在基类中的其他重载函数会被隐藏。
- 如果我们改变基类中一个函数的特征,所有使用该函数名的基类版本都会被隐藏。
非自动继承的函数
不是所有的函数都能自动从基类继承到派生类中。
- 构造函数和析构函数用来处理对象的创建和析构操作,构造和析构函数只知道对它们的特定层次的对象做什么,也就是说构造函数和析构函数不能被继承,必须为每一个特定的派生类分别创建。
- operator=也不能被继承,因为它完成类似构造函数的行为。
也就是说尽管我们知道如何由=右边的对象如何初始化=左边的对象的所有成员,但是这个并不意味着对其派生类依然有效。
多重继承
单继承:指每个派生类只直接继承了一个基类的特征
多继承:指多个基类派生出一个派生类的继承关系,多继承的派生类直接继承了不止一个基类的特征
我们可以从一个类继承,我们也可以能同时从多个类继承,这就是多继承。
多重继承只需要将多个继承的基类使用逗号(,)分隔开即可,分别书写基类继承方式和继承基类名
多继承会带来一些二义性的问题, 如果两个基类中有同名的函数或者变量,那么通过派生类对象去访问这个函数或变量时就不能明确到底调用从基类1继承的版本还是从基类2继承的版本
虚继承和虚基类
菱形继承
菱形继承是多继承中的一种情况
两个派生类继承同一个基类而又有某个类同时继承者两个派生类,这种继承被称为菱形继承(或者钻石型继承)。
菱形继承继承所带来的问题:
-
调用二义性
羊继承了动物的数据和函数,鸵同样继承了动物的数据和函数,当草泥马调用函数或者数据时,就会产生二义性。解决方法:通过指定调用那个基类的方式来解决
-
重复继承导致的空间浪费
草泥马继承自动物的函数和数据继承了两份,其实我们应该清楚,这份数据我们只需要一份就可以
解决方法:采用虚基类
虚继承和虚基类
在多重继承中,如果发生了如:类B继承类A,类C继承类A,类D同时继承了类B和类C,最终在类D中就有了两份类A的成员,这在程序中是不能容忍的,当然解决这个问题的方法就是利用虚继承。
虚继承(Virtual Inheritance)
为了解决多继承时的命名冲突和冗余数据问题,C++ 提出了虚继承,虚继承使得在派生类中只保留一份间接基类的成员。
虚继承的目的是让某个类做出声明,承诺愿意共享它的基类。
- 虚派生只影响从指定了虚基类的派生类中进一步派生出来的类,它不会影响派生类本身
- 在上图中,当定义 D 类时才出现了对虚派生的需求,但是如果 B 类和 C 类不是从 A 类虚派生得到的,那么 D 类还是会保留 A 类的两份成员。
在继承方式前面加上 virtual 关键字就是虚继承
C++标准库中的 iostream 类就是一个虚继承的实际应用案例。
iostream 从 istream 和 ostream 直接继承而来,而 istream 和 ostream 又都继承自一个共同的名为 base_ios 的类,是典型的菱形继承。
此时 istream 和 ostream 必须采用虚继承,否则将导致 iostream 类中保留两份 base_ios 类的成员。
虚基类(Virtual Base Class)
虚继承的目的是让某个类做出声明,承诺愿意共享它的基类。
其中,这个被共享的基类就称为虚基类(Virtual Base Class)
- 本例中的 A 就是一个虚基类。
- 在这种机制下,不论虚基类在继承体系中出现了多少次,在虚基类的派生类(如:D)中都只包含一份虚基类的成员。
继承后虚基类成员的可见性
在虚继承的最终派生类中只保留了一份虚基类的成员,所以该成员可以被直接访问,不会产生二义性,此时访问时可以直接访问而不需要指定作用域。
此外,如果虚基类的成员只被一条派生路径覆盖(子类重写父类成员),那么仍然可以直接访问这个被覆盖的成员,但是如果该成员被两条或多条路径覆盖了(多个子类中都重写父类成员),那就不能直接访问了,此时必须指明该成员属于哪个类(此时成员属于继承的父类而不是虚基类,并非虚继承)。
派生类中的的重定义(重写虚基类成员)成员比虚基类的成员优先级更高
假设 A 定义了一个名为 x 的成员变量,当我们在 D 中直接访问 x 时,会有三种可能性:
- 如果 B 和 C 中都没有 x 的定义,那么 x 将被解析为 A 的成员,此时不存在二义性。
- 如果 B 或 C 其中的一个类定义了 x,也不会有二义性,派生类的 x 比虚基类的 x 优先级更高。
- 如果 B 和 C 中都定义了 x,那么直接访问 x 将产生二义性问题。
虚基类表和虚基类指针
普通继承和虚继承的对象内存图是不一样的,其中包含有虚基类表和虚基类指针
虚继承的派生类结构:
BigBase 菱形最顶层的类(虚基类),内存布局图没有发生改变。
Base1和Base2通过虚继承的方式派生自BigBase,这两个对象的布局图中可以看出编译器为我们的对象中增加了一个虚基类指针vbptr (virtual base pointer),vbptr指向了一张表,这张表保存了当前的虚指针相对于虚基类的首地址的偏移量。
Derived派生于Base1和Base2,继承了两个基类的vbptr指针,并调整了vbptr与虚基类的首地址的偏移量。