目录
一 概念
什么是多态
代码展示
二 虚函数
什么是虚函数呢?
虚函数的条件?
三 虚函数调用的原理
虚表
类A的虚表
对象A的存储模型
单继承情况
多继承的场景
关于生成虚表的时机
运行时决议和编译时决议
运行时决议编译时决议
各种函数类型和多态
多态与析构函数
多态与构造函数
多态与static
多态与inline
虚函数的访问速度
多态行为
四 c++11标准中对于继承的两个关键字——final和override
1 override
2 final
五 抽象类
纯虚函数
纯虚类(接口类)
接口继承和实现继承
如何实现多态性
多态是同一个行为具有多个不同表现形式或形态的能力
比如说,在日常的生活中,同样是买票这个行为。可能因为身份的不同,造成对同一个买票行为,老师,学生,退伍军人,普通人是不同的。比如老师学生打折,退伍军人优先购票,普通人没有折扣。
以上的多态行为是依靠虚函数来实现的
被virtual关键字修饰的类成员函数是虚函数
从函数的声明和定义的角度出发,我们需要:virtual+三同(同函数名 同参数 同返回值)。总结就是完成虚函数的重写和覆盖。如果没有完成重写,那么就是重定义。
从使用的角度出发,我们需要用父类的指针或者引用去调用虚函数
关于为什么会有上述的条件,等我们了解了虚函数的原理之后,才能更好地理解。这一点在之后的篇幅中讲解。
特例:
1子类的虚函数不加上virtual依然是构成虚函数的重写的。可以这样理解:因为子类继承了父类的,同样也使用了父类的virtual。其实虚函数是一个接口继承,比较关心的是实现。
2 对于返回值。子类的返回值可以和父类不同,但是必须是父子关系的指针或者引用才可以。(本身的父子关系或者其他能被识别出来的父子关系)
3 关于三同中的参数。主要要求类型相同。有时候值不一样是没有关系的。
我们从虚函数的条件出发,来探讨一下虚函数的原理
virtual关键字:类中被virtual关键字修饰的函数是虚函数。
虚函数存在于哪里呢?虚函数存在于公共的公共的代码段,但是在虚表(虚函数表简称虚表)中会存放虚函数指针。
什么是虚表?用来存储虚函数指针的一个指针数组,一般情况下是以0结尾。
每一个类都有一个虚表。用来存储自己的虚函数的指针。
如果一个类中有一个虚函数,这个虚表指针就只有一个指针,如果有多个就有多个。要保证把该类中的所有的虚函数指针都存储在虚表中。
通过虚表的指针,找到对应位置的虚函数,完成对该函数的调用。
从简单的入手,再去讨论复杂的情况。
当用类A实例化一个a对象的时候,肯定需要找到对应的虚表。怎么找?用虚表指针来寻找
来观察一下
对象a中开头保存一个指针,是虚函数表指针,指向虚函数表。因此我们需要用指针或者引用来调用。(根据平台的不同 指针大小不同 并且根据平台的不同 有可能放在对象的最前面 也有可能会被放在对象的最后们),之后保存了一个_a
存放:虚表一般存放在常量区,也就是代码段
class A
{
public:
virtual void func1()
{
cout << "A::func1" << endl;
}
void func2()
{
cout << "A::func2" << endl;
}
int _a;
};
class B :public A
{
public:
virtual void func1()
{
cout << "B::func1" << endl;
}
void func3()
{
cout << "B::func3" << endl;
}
int _b;
};
int main()
{
A a;
B b;
return 0;
}
在继承的体系下,B中的虚表有两部分构成,一部分是父类的,一部分是子类的。
对于父类的,会拷贝一份A的虚表到自己的虚表中。如果完成了重写,那么对应的就会重新生成一个虚函数,对应的虚表中指向这自己新生成的虚函数。否则,是和A公用的。
所以对应的,派生类中的虚表应该是这样组成的:1 父类的那一部分虚表 2 子类的虚表如果重写了虚函数就去覆盖父类的,没有就公用。3 自己特有的放在最后
(重写也叫作覆盖,重写是语法层面分析的,覆盖是原理)
重写
没有重写
对于多继承的场景
多继承的话,需要注意的是未被重写的函数会被放在第一个继承的父类的虚函数表的最后。被重写的同样是去覆盖的。如果两个父类都有相同的virtual func1的话,那么两份都要被覆盖
虚表在编译的时候就已经被确定了。因此是先于构造函数和初始化列表的。这里有一位博主讲的很详细:
【踩坑记录】虚函数表是什么时候生成的,虚函数表地址是什么时候给对象的;虚表指针是在构造函数之前就给了对象了;_怎么这么帅啊的博客-CSDN博客_虚表是什么时候生成的
因此,知道了虚表生成的时机,一些对象模型,我们可以理解,实现多态,首先要在编译的时候生成对应的虚表。然后我们通过父类的指针或者引用去调用,实现绑定。这是运行时决议。即运行时确定调用函数的地址。
普通的函数是编译时决议,通过函数的类型就可以了
继承体系下,析构函数会被统一处理成destructor。便于实现多态。并且建议这么写。因为之后析构的时候,子类的对象给父类的指针。根据子类的对象的不同实现不同的析构行为。这一行为本身就是多态。这一个在继承的时候说过了。
构造函数包含拷贝构造函数,构造函数。构造函数是不能实现成多态的。因为时机不同。多态的原理就是运行的时候,去已经生成的虚表里面找对应的函数。但是构造函数在虚表生成之后。
static没有this指针,无法实现运行时调用。static修饰的无法实现多态行为
视情况而定。因为inline函数没有地址,是直接被展开的,但是虚函数表中需要存放对应的地址实现多态。可以实现。
但是实际上是可以的。因为inline只是一个编译器的建议。当一个函数是inline的时候,多态调用中,inline就失效了。
如果一个函数是虚函数,是和普通函数一样快的(前提是没有实现多态:用对象调用,没有用指针或者引用调用)
但是如果实现了多态,由于调用的时候还需要去虚表中寻找,因此会变慢的
多态行为有两种,一种是静态的,一种是动态的。
静态的:编译时决议。函数重载。
动态的:运行时决议。多态调用。
加在子类的虚函数中,用于检查是否完成了对父类函数的重写。如果没有完成就会报错
virtual void func1() override
最终类。一个类不想被继承的话,带上这个关键字,虚函数就不能被重写了。
但是虚函数的目标就是实现多态,不能重写虚函数就没意义了。因此这个会用的很少。
基类中,没有函数的实现体,只有=0来标识,在编译的时候告诉编译器“我这个是个纯虚函数”。
定义纯虚函数,其实是为了实现多态性。派生类必须重写纯虚函数,否则派生类仍然是一个抽象类。
class Car
{
public:
virtual void func() = 0;
};
有纯虚函数的类就是纯虚类。比如Car类。并且如果子类继承了Car,不重写纯虚函数的话,子类也是抽象类。
纯虚类不能实例化出对象,因此不能作为返回值类型和传参来使用。但是可以作为指针(此时没有完成初始化)
因为纯虚类具备给其它函数复用的共有特征,体现了接口继承,因此也被称作接口类。
虚函数这种一般都是接口继承,接口复用,但是实现各自不同。
普通函数则是一种实现继承的体现。普通的成员函数,继承了,那么实现方法同时也被继承了。
派生类(只要有纯虚函数就是抽象类)中必须重写纯虚函数,否则派生类仍然是一个抽象类。只有重写了才能调用的时候实现多态。