多态的概念:通俗来说,就是多种形态,具体点就是去完成某个行为,当不同的对象去完成时会产生出不同的状态。
比如买票这个行为,当普通人买票时,是全价买票;学生买票时,是半价买票;军人买票时是优先买票
并没有构成多态,形参p对象,全部调用了Person类的成员函数。
这时候就需要使用虚函数来构成多态。
梳理一下,多态的条件:
而重写的条件
3. 父子类中的函数都是虚函数。
4. 函数名参数返回值都要相同(有一个例外,那就是协变,基类的虚函数返回基类指针或引用。派生类指针或引用返回派生类指针或引用)
原本都会指向Person类的成员函数,但是当继承类中对虚函数进行重写,基类的指针或引用去调用这个虚函数。此时这个指针或引用指向谁就调用谁的虚函数。这个基类是个相对基类。
即不满足多态,p调用函数,p是什么类型就调用哪个类型的函数。而满足多态,基类的指针或引用指向谁,就调用谁的虚函数。
但是注意,派生类中的虚函数可以不用写virtual。会继承下来,但是我觉得这是C++不严格的地方。显示的带上virtual更好。
虚函数的返回值可以不相同,但是必须满足,基类的虚函数返回值为基类的指针或引用,派生类的虚函数返回值为派生类的指针或引用。我们称它为协变
派生类的析构函数和基类的析构函数实际是构成隐藏的,是因为编译器将析构函数全部定义为destruct。析构函数没有返回值,没有参数,参数名相同。最好将基类的析构函数定义为虚函数,那么派生类的析构函数也会成为虚函数(最好加上virtual,不加也可以),构成重写。
为什么一定需要构成重写呢?
假如有这样一种场景
person类型的指针,指向一个student对象。那么就会发生内存泄漏
将函数声明为虚函数,子类进行重写。
此时不再看P的类型,而是看p指向的是什么对象,然后调用student的析构函数,然后自动调用基类的析构函数。
修饰一个函数,表示该函数不能被重写(我们不写,子类会默认带上)
修饰一个类,表示该类不能被继承
检查派生类是否重写了某个虚函数,如果没有则报错
在虚函数的后面写上 =0 ,则这个函数为纯虚函数。包含纯虚函数的类叫做抽象类,抽象类是不能实例化出对象的。
而派生类直接继承也不行(直接继承你也是抽象类),必须在对它纯虚函数进行重写之后才能实例化出对象。
什么适合被实例化出抽象类呢?人,植物,车,一些很宽泛的概念
,他们这个抽象类中有一些基本的概念,人的职业,植物的类别,车的品牌。然后让派生类继承,实现出具体的行为(人->教师,植物->牡丹,车->奔驰)。
而这样的行为也可以展现多态。因为派生类对函数完成了重写,基类的指针或者引用调用。
抽象类体现了接口继承,接口继承,当纯虚函数是一个声明的时候,我主要继承你的,返回值,函数名,参数。
而实现继承就是,你是一个完整的函数,我继承你就是为了你里面的实现,我不需要在写,直接复用你的。
32位下,默认对齐数为4,此时类有一个虚函数表指针,还有一个int类型变量,所以sizeof大小为8。
此时vfptr数组指针,指向一个虚函数表,这个虚函数表实际上是一个数组,他是一个虚函数指针数组,即 数组里面存储着指针,指针指向一个个函数。
当对象没有初始化的时候,虚函数表也没有初始化,说明对象里面的虚函数表,是在对象初始化的地方才初始化的。他早早就已经创建,初始化就是把数组首元素的值给你就好了。
时刻记住,虚函数表是一个指针数组,一个个指针指向了代码段中的虚函数。func4不是虚函数,所以不在表内。
透过内存来看一下分布
跟前面菱形继承,没有关系!!!!
菱形继承是使用虚继承来解决数据冗余和二义性,使用虚基指针指向一个虚基表,虚基表里面存储着当前地址距离虚基类对象的偏移量,让原来的地址加偏移量就可以找到虚基类对象。
而这里的虚函数表指针,当对象初始化的时候,虚函数表也才会初始化,而且虚函数表只有一张。很显然他是和虚函数一样,都存在代码段中。
可以通过打印地址,来验证
理论上可以 (int)b
但是不支持
可以看出显然是和代码段更加接近。
虽然他是在对象初始化的时候初始化,那么他是在什么时候生成的呢,在编译的时候生成的。
同一个类型用一张虚函数表,这个没有问题。所以子类也是独有一个虚函数表。需要注意的是,假如你多继承,那就是继承多张。继承是复用,而不是共用。
所以可以这么理解,子类直接将整个虚函数表深拷贝下来。当有虚函数被重写,直接在上面覆盖掉原来的虚函数地址。没有被覆盖的就留下。所以重写是语法上的概念,而覆盖是系统底层的概念。那假如子类一个虚函数都没有重写呢?虽然虚函数的地址都没变,但是还会单独生成一个虚函数表。
梳理一下,派生类虚表的生成过程,父类中有虚函数,所以子类先会单独生成一个虚函数表,然后深拷贝下来,假如子类重写了某个虚函数,将重写后的虚函数地址覆盖原来的地址。没有重写的地址就不变。
怎么实现的多态呢?子类中重写了父类的虚函数,指向或引用子类对象的父类的指针或引用调用这个虚函数。
为什么么非得是指针或引用呢?
当你是一个普通对象。那肯定是什么类型就调用哪个类型的函数。
A a 或 B b
即使发生切片 ,由于a对象里面永远是基类的虚函数表,他想实现多态都没处实现
A a=b
而基类类型指针或引用,指向一个子类对象,这时看到的就是子类对象的虚函数表。这样就能调用子类的虚函数。
那么假如有多个虚函数,我调用了不同的虚函数,底层是怎么实现的呢?
也很简单,按照顺序+4字节即可找到。而他的地址就是按声明的顺序放着的。
所以什么是多态呢?
多态分为静态多态和动态多态,静态多态编译时就确定,动态多态是运行时在确定。
函数重载,例如
int i=1;
double j=1.1;
cout<<i;
cout<<j;
函数重载了double1类型和int类型,所以可以输出不同类型。(当然,先实现了运算符重载)。程序编译期间确定了行为。
子类对父类的虚函数进行重写,父类的指针或引用调用这个虚函数。当程序运行起来时,才通过虚函数表来调用虚函数。
单继承中,刚才研究了,子类继承父类的虚函数表,假如有重写了某个虚函数,会直接覆盖掉地址,假如没有重写就保留。那么假如子类自己新增了呢。
class Base
{
public:
virtual void Func1()
{
cout << "Base::Func1()" << endl;
}
virtual void Func2()
{
cout << "Base::Func2()" << endl;
}
private:
int _b = 1;
};
class Derive:public Base
{
public:
virtual void Func1()
{
cout << "Derive::Func1()" << endl;
}
virtual void Func3()
{
cout << "Derive::Func3()" << endl;
}
void Func4()
{
cout << "Derive::Func4()" << endl;
}
private:
int _d = 2;
};
func1覆盖,func2保留,自己写的func3,func4在哪呢?
通过监视窗口看一下,好家伙,func3,func4影子都没有。
不用动脑子都知道,肯定是存在的,从内存角度看一下。
打印一下他的地址,但是该怎么传参呢。
然后main函数中可以调用,注意强转
其实我们既然拿到了函数的地址,那么就可以突破限制,直接调用这个函数。(不在像以前一样只有对象,或者其指针才能调用,那是语法上的概念)
两个对象都调用一下,做个对比
说清楚后,再来看多继承下的虚函数表
#include
using namespace std;
class Base
{
public:
virtual void Func1()
{
cout << "Base::Func1()";
}
virtual void Func2()
{
cout << "Base::Func2()";
}
private:
int _b = 1;
};
class Base1
{
public:
virtual void Func1()
{
cout << "Base1::Func1()";
}
virtual void Func2()
{
cout << "Base1::Func2()";
}
private:
int _b1 = 2;
};
class Derive :public Base,public Base1
{
public:
virtual void Func1()
{
cout << "Derive::Func1()";
}
virtual void Func3()
{
cout << "Derive::Func3()";
}
private:
int _d = 2;
};
int main()
{
Base b;
Base1 b1;
Derive d;
cout << sizeof(d) << endl;
return 0;
}
先口算一下,d有多大。d继承,b和b1的两个虚函数表,8+8+4=20,对齐数为4所以直接放。
打印出来确实是20.
子类重写了,func1,继承了func2,自己写的一个虚函数func3。
func2处于下标1位置,一个是Base类型,一个是Base1类型,在两个虚函数表中地址不同,这是正常的。
在前面我们知道由于子类自己写的虚函数,但是监视窗口不显示,继承的func3,但是内存中是有对应地址的,第一个疑惑的点,那么这个自己写的虚函数地址会在这两个虚函数表当中哪一个?还是都有呢?
第二个疑惑的点?子类重写了继承下来的func1,为啥是两个地址,难道不该是一个地址直接覆盖两个虚函数表吗?最诡异的是两个地址,他还调用的是相同的函数
先解决第一个问题,在前面我们写了一个打印虚函数表的函数,这里在复用一下,看看他在哪?
可以看到,自己实现的那个虚函数,地址是放到了Base类的虚函数表之中。
而与此同时,第二个问题还是没能得到解答,因为在内存中,重写之后的Func1仍旧是两个地址,但调用同一个。
然后在经过F10调试,执行的时候也确实是进入了同一个函数。
那在直接对它取地址,好家伙,不得了了,3套地址。
所以这里我们可以推理得,虽然虚函数表里的地址不一样,但是在汇编层面他们会jmp到一个地址处完成对同一个函数的调用。
B:虚函数表简称虚表,虚基表是为了解决菱形继承引入的,里面存的是偏移量
D正确,父类和子类,甚至子类中没有重写任何虚函数,都会生成不同的虚函数表。
参数列表初始化的顺序是声明的顺序,因为是先继承的B,在继承的C。
假如先继承的C,在继承B,那么就会打印 A C B D
p为B类型,当p去调用test,由于test没有重写,是直接继承下来,所以里面的函数原封不动,this指针类型仍然是A,此时把p赋值给A*的this,就相当于
那不是应该打印B->0吗,错,其实虚函数是一种接口继承,他将你的参数,返回值,形参列表继承下来所以B类中,形参的缺省参数不起作用,还是A中的val=1,所以打印的是B->1。
A* a=new B;
当delete a
时不构成多态就只会调用A的析构函数,从而造成内存泄漏。定义成虚函数,由于构成重写(析构函数名destructor),基类的指针调用,所以构成多态,从而去调用B的析构函数,而B的析构函数又会自动调用A的析构函数。所以就解决了内存泄漏问题。