什么是多态?
通俗的,一个事物有多种状态,在C++里是指一个基类成员函数被不同的派生类或者基类调用,有不同的结果。用基类的指针或引用操纵多个类型的能力被称为多态
多态分为静态多态和动态多态
静态多态是在编译期间完成,根据函数参数实参判断需要调用的函数。以此形成了函数重载技术和泛型编程。
动态多态则是利用虚函数实现了运行时的多态,也就是说在系统编译的时候并不知道程序将要调用哪一个函数,只有在运行到这里的时候才能确定接下来会跳转到哪一个函数的栈帧。
什么时候形成动态多态
- 基类必须有虚函数,并且派生类对虚函数进行重写。
- 通过基类指针调用虚函数。
这里又有虚函数和重写的概念,声明虚函数只需在其返回值前加virtual关键字。
在派生类重写虚函数必须保持函数原型一致,包括返回值,参数,函数名,可以不加virtual修饰。
两个例外:虚析构(最好将基类析构声明为虚拟),协变——基类的虚函数返回基类指针或引用,派生类虚函数返回派生类指针或引用。
那些函数不能声明为虚拟的:https://www.cnblogs.com/noble/p/4144011.html
函数重载,同名隐藏,重写
三个概念看起来很类似,都是对同名成员的处理。
函数重载实现了在同一作用域内的同名函数根据不同的实参进行不同调用的技术。
同名隐藏是在继承体系中,派生类和基类有同名成员,使用就近原则的调用。
重写是针对派生类的虚函数,且基类和派生类虚函数原型一致,是动态多态的条件。
抽象类
指明一个虚拟函数只是提供了一个 可被子类型改写的接口 但是 它本身并不能通过虚拟机制被调用 这就是纯虚拟函数 。纯虚拟函数的声明如下所示
virtual ostream& print( ostream&=cout ) const = 0;
包含 或继承 一个或多个纯虚拟函数的类被编译器识别为抽象基类 ,试图创建一个抽象基类对象会导致编译时刻错误 ,类似地 通过虚拟机制调用纯虚拟函数也是错误的.
多态调用原理
typedef void(*pFun)();
class A {
public:
virtual void fun1() {
cout << "A::fun1" << endl;
}
virtual void fun2() {
cout << "A::fun2" << endl;
}
int _a;
};
class B :public A
{
public:
//子类重写了fun2,有自己的fun3,继承了fun1
virtual void fun2() {
cout << "B::fun2" << endl;
}
virtual void fun3() {
cout << "B::fun3" << endl;
}
int _b;
};
void print(A& a, int count) {
//将基类对象前4字节解引用,找到虚表地址
pFun* pf = (pFun*)(*((int*)&a));
while (count--) {
(*pf)();
pf++;
}
cout << endl;
}
int main()
{
A* pBase;
A a;
B b;
a._a = 3;
b._a = 1;
b._b = 2;
cout << sizeof(B) << endl;
print(a, 2);
print(b, 3);
pBase = &b;
pBase->fun2();
//虽然强转为基类指针,但是传入的还是派生类对象地址,使用派生类虚表进行调用
pBase = (A*)&b;
pBase->fun2();
pBase = &a;
pBase->fun2();
}
执行结果:
12
//a对象的虚表
A::fun1
A::fun2
//b对象的虚表
A::fun1 //继承的
B::fun2 //修改的
B::fun3 //自己的
B::fun2
B::fun2
A::fun2
可以发现虚表构建过程:派生类首先继承基类虚表,并替换指向自己的虚函数,append自己的虚函数到虚表最后。
派生类b对象模型:
首4字节存放虚表地址,其次放基类成员,再放本类成员。
0x0073FD20 04 9c 0d 01 .?..
0x0073FD24 01 00 00 00 ....
0x0073FD28 02 00 00 00
剖析父类指针调用虚函数反汇编:
pBase = &b;
pBase->fun2();
010D5CDC mov eax,dword ptr [pBase] //取this指针地址
010D5CDF mov edx,dword ptr [eax] //获取虚表地址
010D5CE1 mov esi,esp
010D5CE3 mov ecx,dword ptr [pBase]
010D5CE6 mov eax,dword ptr [edx+4] //虚表地址+偏移量
010D5CE9 call eax //调用虚函数
结论:之所以使用基类指针能调用不同的类中的虚函数,是因为不同的类对象内有一份自己的虚表,而子类和父类的虚表结构相同,意味着重写的虚函数在虚表内的偏移量相同。在调用虚函数时用虚表地址+偏移量调用后,刚好就能调用不同类的虚函数。
进一步探究菱形虚拟继承对象模型
class A {
public:
virtual void fun1() {
cout << "A::fun1" << endl;
}
virtual void fun2() {
cout << "A::fun2" << endl;
}
int _a;
};
class B :virtual public A
{
public:
//重写fun2,自己fun3
virtual void fun2() {
cout << "B::fun2" << endl;
}
virtual void fun3() {
cout << "B::fun3" << endl;
}
int _b;
};
class C :virtual public A
{
public:
//自己fun4,fun5
virtual void fun4() {
cout << "C::fun4" << endl;
}
virtual void fun5() {
cout << "C::fun5" << endl;
}
int _c;
};
class D :public B, public C {
public:
//D再次重写fun2,有自己的fun6
virtual void fun2() {
cout << "D::fun2" << endl;
}
virtual void fun6() {
cout << "D::fun6" << endl;
}
int _d;
};
void print(A& a, int count) {
//将基类对象前4字节解引用,找到虚表地址
pFun* pf = (pFun*)(*((int*)&a));
while (count--) {
(*pf)();
pf++;
}
cout << endl;
}
int main()
{
A* pBase;
D d;
d._d = 4;
d._c = 3;
d._a = 1;
d._b = 2;
cout << sizeof(D) << endl;
pBase = &d;
cout << "B虚表:" << endl;
pBase = (A*)((int*)pBase - 7);
print(*pBase, 2);
cout << "C虚表:" << endl;
pBase = (A*)((int*)pBase + 3 );
print(*pBase, 2);
cout << "基类A虚表:" << endl;
pBase = &d;
print(*pBase, 2);
}
执行结果:
36
B虚表:
B::fun3
D::fun6C虚表:
C::fun4
C::fun5基类A虚表:
A::fun1
D::fun2
派生类d对象模型:
0x0073FC08 e0 9b 0f 00 ??.. //B虚表
0x0073FC0C 08 9c 0f 00 .?.. //B偏移表
0x0073FC10 02 00 00 00 ....
0x0073FC14 ec 9b 0f 00 ??.. //C
0x0073FC18 14 9c 0f 00 .?..
0x0073FC1C 03 00 00 00 ....
0x0073FC20 04 00 00 00 ....
0x0073FC24 fc 9b 0f 00 ??.. //A
0x0073FC28 01 00 00 00
其中0x0073FC18地址的内容:
0x000F9C14 fc ff ff ff ?... //-4,相对于本类首地址的偏移
0x000F9C18 0c 00 00 00 //12,相对基类的偏移
结果分析:
对象模型遵循2个原则
- 首先d是多继承的,所以先放B,C再放本类,再放基类成员,把自己的虚函数添加到第一张虚表后。
- B和C是虚拟继承A,在自己类里有偏移表,并且有虚函数表,且虚函数表仅存BC类有自己独有的虚函数。
多继承结构虚表构建过程:先继承基类虚表,修改重写的虚表,将自己的虚函数添加到第一张虚表后(没有自己的虚表)。
虚拟继承虚表构建过程:在基类虚表修改重写的虚函数,添加自己的虚函数到自己的虚表