参考资料:阿秀
一、面向对象三大特性
封装:将数据和代码捆绑在一起,避免外界干扰和不确定性访问
继承:让某种类型对象获得另一个类型对象的属性和方法
多态:同一种事务表现出不同事务的能力,即:向不同对象发送同一消息,不同的对象在接收时会产生不同的行为
重载实现编译时多态,虚函数实现运行时多态。
实现多态的两种方式:
- 覆盖:子类重新定义父类的虚函数做法
- 重载:允许存在多个同名函数,而这些函数的参数表不同(参数个数不同、或者参类型不同、或者两者都不同)
二、虚函数
在基类的函数前加上virtual关键字,在派生类中重写该函数,运行时将会根据所指对象的实际类型来调用相应的函数,如果对象类型是派生类,就调用派生类的函数,如果对象类型是基类,就调用基类的函数。
底层原理:
- 虚表: 虚函数表的缩写,类中含有virtual关键字修饰的方法时,编译器会自动生成虚表
- 虚表指针: 在含有虚函数的类实例化对象时,对象地址的前四个字节存储的指向虚表的指针
上图展示了虚表和虚表指针在基类对象和派生类对象中的模型,那么多态具体是如何实现的呢?
1. 对象初始化
- 编译器会自动为每个含有虚函数的类生成一份虚表,该表时一个一维指针数组,虚表中保存了虚函数的入口地址。
- 编译器会在每个对象的前四个字节中保存一个虚表指针vptr,指向对象所属类的虚表。在构造时,根据对象的类型初始化虚指针vptr,从而让虚指针指向正确的虚表。
- 在派生类定义对象时,程序会自动调用构造函数,在构造函数中创建虚表并对虚表初始化。
2. 虚指针指向
- 当派生类对基类的虚函数没有重写时,派生类的虚表指针指向的是基类的虚表;
- 当派生类对基类的虚函数重写时,派生类的虚表指针指向的是自身的虚表;
- 当派生类中有自己的虚函数时,在自己的虚表指针中将此虚函数地址添加在后面。
这样指向派生类的基类指针在运行时,可以根据派生类对虚函数重写情况动态进行调用,从而实现多态性。
构造函数和析构函数可以声明为虚函数吗?
构造函数不能定义为虚函数,析构函数可以为虚函数,并且一般情况下基类析构函数都要定义为虚函数。
构造函数:每个含有虚函数的类都有一个虚表指针,指向虚函数表。如果构造函数时虚函数,就需要通过虚表指针寻找虚函数表,从而找到对应的虚函数实现。但是类对象还没有初始化,就没有虚表指针,找不到虚函数,所以构造函数不能时虚函数。
析构函数:只有在基类析构函数是虚函数时,调用delete操作符销毁指向派生类的基类指针时,才能准确调用派生类的析构函数,而派生类的析构函数又自动调用基类的析构函数,这样整个派生类的对象完全被释放。
三、纯虚函数
虚函数和纯虚函数的区别?
- 虚函数是为了实现动态编译产⽣的,目的是通过基类类型的指针指向不同对象时,自动调用相应的、和基类同名的函数(使⽤同⼀种调用形式,既能调用派生类又能调用基类的同名函数)。虚函数需要在基类中加上 virtual修饰符修饰,因为virtual会被隐式继承,所以子类中相同函数都是虚函数。当⼀个成员函数被声明为虚函数之后,其派生类中同名函数自动成为虚函数,在派生类中重新定义此函数时要求函数名、返回值类型、参数个数和类型全部与基类函数相同。
- 纯虚函数只是相当于⼀个接口名,但含有纯虚函数的类不能够实例化。
纯虚函数首先是虚函数,其次没有函数体,取而代之使用“=0”代替。
它的函数指针会被存在虚函数表中,由于纯虚函数并没有具体的函数体,因此他在虚函数表中的值为0,其他有函数体的虚函数则是函数的具体地址。
一个类中如果存在纯虚函数,称为抽象类,抽象类不能用于实例化,一般用于定义一些公有方法。子类继承抽象类也必须实现其中的纯虚函数才能实例化对象。
四、虚拟继承
一个类可以从多个基类(父类)继承属性和行为。在C++等支持多重继承的语言中,一个派生类可以同时拥有多个基类。
多重继承可能引入一些问题,如萎形继承问题,比如当一个类同时继承了两个拥有相同基类的类,而最终的派生类又同时继承了这两个类时,可能导致二义性和代码设计上的复杂性。为了解决这些问题,C++ 提供了虚继承,通过在继承声明中使用 virtual 关键字,可以避免在派生类中生成多个基类的实例,从而解决了菱形继承带来的二义性。
举个🌰:
#include <iostream> using namespace std; class A{} class B : virtual public A{}; class C : virtual public A{}; class D : public B, public C{}; int main() { cout << "sizeof(A):" << sizeof A <<endl; // 1,空对象,只有⼀个占位 cout << "sizeof(B):" << sizeof B <<endl; // 4,⼀个bptr指针,省去占位,不需要对⻬ cout << "sizeof(C):" << sizeof C <<endl; // 4,⼀个bptr指针,省去占位,不需要对⻬ cout << "sizeof(D):" << sizeof D <<endl; // 8,两个bptr,省去占位,不需要对⻬ }
上述代码所体现的关系是,B和C虚拟继承A,D公有继承B和C,这种方式是⼀种菱形继承或者钻石继承,可以用下图来表示:
虚拟继承的情况下,无论基类被继承多少次,只会存在一个实体。
虚拟继承基类的子类中,子类会增加某种形式的指针,或者指向虚基类子对象,或者指向一个相关表格;表格中存放的不是虚基类子对象的地址,就是其偏移量,此类指针被称为bptr。如果即存在vptr又存在bptr,某些编译器会将其优化,合并为一个指针。