什么是多态?
首先看语法,满足两个条件:虚函数的重写和基类指针或引用。
1.虚函数的重写:以下代码继承函数前加virtual关键字。(继承要求三同:函数名,参数和返回值的类型)
2. 基类指针或引用:以下代码“A* pa = &bb;”
#include<iostream>
using namespace std;
class A
{
public:
virtual void func1() {
cout << "A::void func1()" << endl;
}
virtual void func2() {
cout << "A::void func2()" << endl;
}
};
class B : public A
{
public:
virtual void func1() {
cout << "B::void func1()" << endl;
}
virtual void func2() {
cout << "B::void func2()" << endl;
}
};
intmain()
{
A aa;
B bb;
A* pa = &bb;
pa->func1();
return 0;
}
再看原理
子类继承时,从更好理解的角度看,先把父类的虚表copy下来,再把需要重写实现的虚函数地址覆盖过去,得到子类的虚表。(那什么是虚表呢?实际上是一个函数指针数组,里面存虚函数 的地址。)
把子类的指针或引用给父类指针或引用,形成切片,此时子类的虚函数表会copy给父类指针,这样就会调用子类自己的某个虚函数。同时一般的父类指针或引用可以调用父类自己的虚函数,这样一来,父类的指针或引用可以调用子类和父类的续保函数,这就叫多态。
那为什么不把子类对象直接赋值给父类,而绕一点,用指针和引用呢?
很简单,看以下代码
void test1()
{
A aa;
B bb;
aa = bb;
aa.func1();//结果是调用A的func1,可见对象的切片不会拷贝虚表
//若拷贝,则父类对象可以调用子类的函数,不合理,但父类指针可以
}
void test2()
{
A aa;
B bb;
A* pa = &bb;
pa->func1();//结果是调用B的func1,可见指针的切片会拷贝覆盖A父类的虚表
}
可见,对象的截断并不会改变虚表,只会改变一般的成员变量。这也很合乎逻辑,因为若果和指针一样,把虚表copy过去,那么父类对象既可以调用自己的函数,也可以调用子类的函数,这显然是不合理的。(C++语法规定,父类只能调用自己的函数,子类可以调用父类)
接下来来到第二个问题:虚表存放在哪里?
以下代码可以验证
#include<iostream>
using namespace std;
class A
{
virtual void func1() {}
virtual void func2() {}
};
class B : public A
{
virtual void func1() {}
virtual void func2() {}
};
int main()
{
int a = 0;
printf("栈:%p\n", &a);
int* b = new int;
printf("堆:%p\n", b);
static int c = 0;
printf("静态区:%p\n", &c);
const char* d = "hello";
printf("常量区:%p\n", d);
A aa;
printf("虚表A:%p\n", *((int*)&aa));//对象里面头四个字节存的是虚表地址
//(int*)&aa是对象的地址,强转为int*,取前4个字节得到对象(本质是函数指针数组,里面存虚函数的地址)的地址,
//虚表地址是对象指针指向的第一个内容,解引用下得到虚表地址
B bb;
printf("虚表B:%p\n", *((int*)&bb));
return 0;
}
显而易见,虚表地址理常量区最近,故存在常量区。这也很合理,因为虚表生成后是不可改变的。
再补充一句,子类的新虚函数地址会加在自己的虚表末尾,但是vs2019的监视窗口看不见。
最后看多继承的虚表
多继承会继承多张虚表,但是监视窗口看不见cc新加的func3,于是我设计了以下代码调用两张表上的函数。
#include<iostream>
using namespace std;
typedef void(*VFT_PTR)();
class A
{
public:
virtual void func1() {
cout << "A::void func1()" << endl;
}
virtual void func2() {
cout << "A::void func2()" << endl;
}
};
class B
{
public:
virtual void func1() {
cout << "B::void func1()" << endl;
}
virtual void func2() {
cout << "B::void func2()" << endl;
}
};
class C : public A, public B
{
public:
virtual void func1() {
cout << "C::void func1()" << endl;
}
virtual void func2() {
cout << "C::void func2()" << endl;
}
virtual void func3() {
cout << "C::void func3()" << endl;
}
};
void PrintVFT(VFT_PTR* table)
{
for (int i = 0; table[i] != nullptr; ++i)
{
cout << table[i] << "->";
VFT_PTR f = table[i];
f();
}
}
int main()
{
C cc;
int vft1 = *((int*)&cc);
cout << "从A那继承的虚表" << endl;
PrintVFT((VFT_PTR*)vft1);
B* pb = &cc;
int vft2 = *((int*)pb);
cout << "从B那继承的虚表" << endl;
PrintVFT((VFT_PTR*)vft2);
return 0;
}
显而易见,C新加的func3会添加在第一张虚表。
然而新的问题又来了,cc的func1,func2只有一个,但是两张表里有两个分别有两个func1,func2,按理来相同的函数只有一份,怎么回事呢?
其实是从第二份虚表调用func1时,编译器会把fun1的地址进行加减,变得和第一份虚表的地址一样,然后调用同一个func1。为什么要这么复杂呢?调用成员虚函数不仅需要虚表,还需要this指针,从第一份虚表调用func1,this指针刚好一样,所以直接传过去就好了,(这也是单继承的this指针传参方法)而第二份虚表调用func1时,this指针值不一样了,所以要进行调整,加减几个字节,再传过去,和func1调用是一个道理。