(动态)多态:当使用基类的指针或引用调用重写的虚函数,当指向父类调用的就是父类的虚函数,指向子类调用的就是子类的虚函数。
不难剖析出要实现多态:
首先要有虚函数的重写,其次要用基类的指针去调用。
那么先简单认识下虚函数&虚函数的重写:
虚函数:类的成员函数加了 virtual 关键字。
虚函数的重写:当在子类中定义了一个与父类完全相同的虚函数时,则称子类的这个函数重写(也称覆盖)了父类的虚函数。
程序一
看下面的代码
#include<iostream>
using namespace std;
class person
{
public:
virtual void Buyticket()
{
cout << "成人-全价" << endl;
}
};
class student :public person
{
virtual void Buyticket()
{
cout << "学生-半票" << endl;
}
};
void func(person& p)
{
p.Buyticket();
}
int main()
{
person p;
student s;
func(p);
func(s);
system("pause");
return 0;
}
不难分析出运行结果是:
成人-全价
学生-半票
这个程序实现了一个简单的多态,显然它符合构成多态的两个条件:
虚函数的重写与基类的指针或引用。
思考
派生类的指针或引用否可行?
当然是不可行的,子类的指针或引用赋给父类时会发生切片行为,父类指向的部分是父类与子类共有的。如果是子类的指针或引用,就有可能出现越界访问。
重载(静态多态),重定义(隐藏),重写(覆盖)各自的定义?
上图列出了,这三个的基本构成条件。要分别认识他们,就需要思考它们的出现实现了什么功能,解决了什么问题。
重载:在同一作用域内函数名相同,参数的不同(类型或个数),根据传的参数来实现不同的功能,例如类里面的构造函数与拷贝构造函数。
重定义:⼦类和⽗类中有同名成员,⼦类成员将屏蔽⽗类对成员的直接访问。相当于从父类继承下来的成员不隐藏掉,要想访问需要加域作用符去访问。
重写:是为了实现多态引入的,子类调用指向子类的虚函数,父类调用指向父类的虚函数。
简单的总结下多态:
C++中虚函数的主要作⽤就是实现多态。简单说⽗类的指针/引⽤调⽤重写的虚函数,当⽗类指针/引⽤指向⽗类对象时调⽤的是⽗类的虚函数,指向⼦类对象时调⽤的是⼦类的虚函数。
构成多态类型决定了你调用的方法,没有构成多态对象决定了调用的方法。
不理解的可以看下面的程序
程序二
#include<iostream>
using namespace std;
class person
{
public:
virtual void Buyticket()
{
cout << "成人-全价" << endl;
}
};
class student :public person
{
virtual void Buyticket()
{
cout << "学生-半票" << endl;
}
};
void func(person p) //参数与程序一不同
{
p.Buyticket();
}
int main()
{
person p;
student s;
func(p);
func(s);
system("pause");
return 0;
}
运行结果是:
成人-全票
成人-全票
程序二与程序一不同的是,程序二的参数成为了值传递,不再是引用于指针,不再构成重载,这样调用结果自然就由对象去决定,p是基类所构建出来的对象,所以调用基类的方法自然就不难理解了。
下面介绍多态的底层实现
多态的实现主要依赖虚表(虚函数表)通过一块连续的内存存储虚函数的地址。这张表解决了继承、虚函数(重写)的问题。在有虚函数的对象实例中都存在⼀张虚函数表,虚函数表就像⼀张地图,指明了实际应该调⽤的虚函数函数。
下面先简单的通过一个简单的例子认识下:(在win10,vs2013下的演示情况,32位)
#include<iostream>
using namespace std;
class Base
{
public:
virtual void func1()
{}
virtual void func2()
{}
private:
int a;
};
void Test1()
{
Base b1;
cout << sizeof(b1) << endl;
}
int main()
{
Test1();
system("pause");
return 0;
}
通过sizeof()求出b1的类型大小是8
为什么?
在base类中我们只是定义了一个整形变量,应该只有4个字节,但是却求出了8个字节。打开监视窗口:
可以看出b1内还存在了一个指针,这个指针指向的位置呢就是虚基表的位置,指针的类型为函数指针数组的指针,他存储了指向虚函数的指针,虚基表内有所有的虚函数指针。
下图更加详细的展示了这一点:
下面呢介绍单继承的对象模型
同样先看代码:
#include<iostream>
using namespace std;
class Base
{
public:
virtual void func1()
{
cout << "Base::func1" << endl;
}
virtual void func2()
{
cout << "Base::func2" << endl;
}
private:
int a;
};
class Derive :public Base
{
public:
virtual void func1()
{
cout << "Derive::func1" << endl;
}
virtual void func3()
{
cout << "Derive::func3" << endl;
}
virtual void func4()
{
cout << "Derive::func4" << endl;
}
private:
int b;
};
//自己实现打印虚表
typedef void(*FUNC)();
void PrintVtable(int Vatable)
{
int* pVatable = (int*)Vatable;
cout << "虚表地址:" << pVatable << endl;
size_t i = 0;
for (i = 0; pVatable[i] != 0; i++)
{
printf("虚函数地址:%p->", pVatable[i]);
FUNC f = (FUNC)pVatable[i];
f();
printf("\n");
}
cout << endl;
}
int main()
{
Base b;
Derive d;
int Vtab_b = *((int*)&b);
PrintVtable(Vtab_b);
int Vtab_d = *((int*)&d);
PrintVtable(Vtab_d);
system("pause");
return 0;
}
同样先展示内存监视:
看基类的虚函数表是没有问题的,但是再看派生类的虚函数时发现只有两个,但我写了四个,看到的两个分别是对基类fun1()的重写,和继承基类的fun2();但自己写的fun3()与fun4()并没有。这块呢可以理解为V自身的问题(BUG)。
在上述代码中我写了虚表的打印函数,看运行结果:
从我们写的打印函数可以看出子类确实是存在4个虚函数的。
再看下对象模型:
多继承的对象模型:
#include<iostream>
using namespace std;
class Base1
{
public:
virtual void func1()
{
cout << "Base1::func1" << endl;
}
virtual void func2()
{
cout << "Base1::func2" << endl;
}
private:
int b1;
};
class Base2
{
public:
virtual void func1()
{
cout << "Base2::func1" << endl;
}
virtual void func2()
{
cout << "Base2::func2" << endl;
}
private:
int b2;
};
class Derive : public Base1, public Base2
{
public:
virtual void func1()
{
cout << "Derive::func1" << endl;
}
virtual void func3()
{
cout << "Derive::func3" << endl;
}
private:
int d1;
};
typedef void(*FUNC) ();
void PrintVTable(int* VTable)
{
cout << " 虚表地址>" << VTable << endl;
for (int i = 0; VTable[i] != 0; ++i)
{
printf(" 第%d个虚函数地址 :0X%x,->", i, VTable[i]);
FUNC f = (FUNC)VTable[i];
f();
}
cout << endl;
}
void Test1()
{
Derive d1;
int* VTable = (int*)(*(int*)&d1);
PrintVTable(VTable);
// Base2虚函数表在对象Base1后⾯
VTable = (int *)(*((int*)&d1 + sizeof (Base1) / 4));
PrintVTable(VTable);
}
int main()
{
Test1();
system("pause");
return 0;
}
然后看对象模型:
观察对象模型可以发现,在多继承时,自身的虚函数,放在了第一个父类的虚表里面。
现在我们可以明白多态在实现的过程中借助了虚表这样的方式,或许还是会有疑惑它借助了虚表没错,但它是如何去用的呢?
看下面的代码:
#include<iostream>
using namespace std;
class person
{
public:
virtual void Buyticket()
{
cout << "成人-全价" << endl;
}
};
class student :public person
{
virtual void Buyticket()
{
cout << "学生-半票" << endl;
}
};
void func(person& p)
{
p.Buyticket(); //A
}
int main()
{
student s;
func(s);
return 0;
}
在A处打断点,并转到反汇编
p.Buyticket();
002058FE mov eax,dword ptr [p]
00205901 mov edx,dword ptr [eax]
00205903 mov esi,esp //检查越界
00205905 mov ecx,dword ptr [p]
00205908 mov eax,dword ptr [edx]
0020590A call eax
0020590C cmp esi,esp //检查越界
0020590E call __RTC_CheckEsp (0201343h) //检查越界
}
对比非多态的反汇编(程序二)
p.Buyticket();
00B9595E lea ecx,[p]
00B95961 call person::Buyticket (0B91005h)
}
可以明显看出多态的调用指令更多,这里p运行时是直接去调指向对象的虚表。当指针p指向不同的对象时,就调用对应的虚表,虚表内的存的时函数指针。
这就是它的底层实现。
通过虚表去认识多态,才能更好的理解多态。
可以思考下面的问题:
在构造函数与析构函数期间,能否调用虚函数?
不能。
构造函数期间,初始化没有完成,对象没有构造好,然后调虚函数,可能会引起越界访问的问题。
析构函数也是同样的原因。
内联函数,构造函数,静态成员函数能否写成虚函数?
不能。
内联函数是展开,并不存在函数地址。
构造函数对象没有初始化完成,虚函数时根据对象的类型来决定调父类还是子类的,对象都没有产生,如何调用虚函数。
静态成员函数是整个类所共有的,并不属于某个对象所以也不需要去动态的进行绑定。
深度探索:
菱形继承的对象模型:
#include <iostream>
using namespace std;
class A
{
public:
virtual void func1()
{
cout << "A::func1()" << endl;
}
virtual void func2()
{
cout << "A::func2()" << endl;
}
public :
int _a;
};
class B :public A
{
public:
virtual void func1()
{
cout << "B::func1()" << endl;
}
virtual void func3()
{
cout << "B::func3()" << endl;
}
public:
int _b;
};
class C :public A
{
public:
virtual void func1()
{
cout << "C::func1()" << endl;
}
virtual void func4()
{
cout << "C::func4()" << endl;
}
public:
int _c;
};
class D : public B, public C
{
public:
virtual void func1()
{
cout << "D::func1()" << endl;
}
virtual void func5()
{
cout << "D::func5()" << endl;
}
public:
int _d;
};
typedef void(*FUNC) ();
void PrintVTable(int* VTable)
{
cout << " 虚表地址>" << VTable << endl;
int** ppVTable = (int**)VTable;
for (int i = 0; VTable[i] != 0; ++i)
{
printf(" 第%d个虚函数地址 :0X%p->", i, VTable[i]);
FUNC f = (FUNC)VTable[i];
f();
}
cout << endl;
}
void Test1()
{
D d;
d.B::_a = 1;
d._b = 2;
d.C::_a = 3;
d._c = 4;
d._d = 5;
PrintVTable(*((int**)&d));
PrintVTable(*((int**)((char*)&d + sizeof(B))));
}
int main()
{
Test1();
system("pause");
return 0;
}
然后看它的对象模型:
观察菱形继承可以发现,它的本质仍然是一个多继承,它也存在菱形继承所特有的问题,代码的二义性和数据的冗余。冗余体现在虚函数的冗余和数据的冗余。
提到菱形继承自然就应该想到虚继承,虚继承是c++专门为解决菱形继承所设计的。
下面来探索菱形继承的虚继承的对象模型
#include <iostream>
using namespace std;
class A
{
public:
virtual void func1()
{
cout << "A::func1()" << endl;
}
virtual void func2()
{
cout << "A::func2()" << endl;
}
public :
int _a;
};
class B :virtual public A
{
public:
virtual void func1()
{
cout << "B::func1()" << endl;
}
virtual void func3()
{
cout << "B::func3()" << endl;
}
public:
int _b;
};
class C :virtual public A
{
public:
virtual void func1()
{
cout << "C::func1()" << endl;
}
virtual void func4()
{
cout << "C::func4()" << endl;
}
public:
int _c;
};
class D : public B, public C
{
public:
virtual void func1()
{
cout << "D::func1()" << endl;
}
virtual void func5()
{
cout << "D::func5()" << endl;
}
public:
int _d;
};
typedef void(*FUNC) ();
void PrintVTable(int* VTable)
{
cout << " 虚表地址>" << VTable << endl;
int** ppVTable = (int**)VTable;
for (int i = 0; VTable[i] != 0; ++i)
{
printf(" 第%d个虚函数地址 :0X%p->", i, VTable[i]);
FUNC f = (FUNC)VTable[i];
f();
}
cout << endl;
}
void Test1()
{
D d;
d.B::_a = 1;
d._b = 2;
d.C::_a = 3;
d._c = 4;
d._d = 5;
}
int main()
{
Test1();
system("pause");
return 0;
}
看它的对象模型:
分析对象模型可以得出:首先对象A,B,C各自都有自己的虚表,显然模型上的A部分就是公共区域,有公共区域,自然就应该有虚继承的内容,在上篇博客演示虚继承时,虚继承存了偏移量,这里分析一下B的虚基表。
有两行内容,第二行存了数据的偏移量,第一行自然是为了指向虚表的位置,它呢是在数据的偏移量加上它存储的偏移量(是一个有符号数,表中的存储是 -4),自然就找到虚表的位置。
可以察觉菱形继承虚继承的对象模型是复杂的,复杂就造成它的访问效率较低,故此它一般很少使用。
以上就是多态的内容。
备注:
1.虚函数与虚继承没有联系,它两只是使用了同一个关键字。虚函数是为了实现重写继而实现多态产生的,虚继承是为了解决菱形继承二义性与数据冗余产生的。
2.上述打印虚表的函数仅仅适用于32位的平台,下面给出,适用于32与64位平台的解决方案
typedef void(*FUNC) ();
void PrintVTable(int* VTable)
{
cout << " 虚表地址>" << VTable << endl;
int** ppVTable = (int**)VTable;
for (int i = 0; VTable[i] != 0; ++i)
{
printf(" 第%d个虚函数地址 :0X%p->", i, VTable[i]);
FUNC f = (FUNC)VTable[i];
f();
}
cout << endl;
}
void Test1()
{
D d;
d.B::_a = 1;
d._b = 2;
d.C::_a = 3;
d._c = 4;
d._d = 5;
PrintVTable(*((int**)&d));
PrintVTable(*((int**)((char*)&d + sizeof(B))));
}
3.在成员函数的形参后⾯写上=0,则成员函数为纯虚函数。包含纯虚函数的类叫做抽象类(也叫接口类),抽象类不能实例化出对象。纯虚函数在派⽣类中重新定义以后,派⽣类才能实例化出对象。
4.友元关系不能继承,也就是说基类友元不能访问⼦类私有和保护成员.
5.基类定义了static成员,则整个继承体系⾥⾯只有⼀个这样的成员。⽆论派⽣出多少个⼦类,都只有⼀个static成员实例。
至此,本篇博客结束,建议与上篇博客继承一起阅读。