虚函数:在理解多态之前,我们需要先来理解什么叫做虚函数?在成员函数之前冠以关键字“virtual”时,此时这个函数被称之为“虚函数”, 当在子类中定义了一个和父类完全一样的虚函数时,可以称为子类的这个函数重写(或者覆盖了)父类的这个虚函数。简单来说,虚函数使方法在基类和派生类中的行为不同。
多态:很多时候,我们希望同一个方法在派生类和基类的行为是不一样的,换句话来说,方法的行为应取决于调用该方法的对象,这种较复杂的行为称之为“多态”。
来看一个例子:
#include<iostream> using namespace std; class person{ public: void fun() { cout<<"I am person"<<endl; } }; class student : public person{ public: void fun() { cout<<"I am student"<<endl; } }; int main() { //Test(); person p; student s; person& p1=p;//基类对象赋给基类引用 p1.fun(); person& p2=s;//派生类对象赋给基类引用 p2.fun(); system("pause"); return 0; }
运行结果为:
我们发现两次调用fun函数都调用了基类的fun函数,但是如果我们将fun函数定义为虚函数呢?
很明显,在fun函数前加上关键字virtual后,两次调用分别调用了基类和派生类的fun; 说明了一个问题:如果方法是通过引用或者指针而不是直接通过对象调用时(对象不能构成多态),如果没有使用关键字virtual,程序将根据引用或者指针的类型来选择方法;如果使用了关键字virtual,程序将根据引用或者指针指向的对象类型来选择方法。
构成多态的两个必要条件:
不构成多态的常规调用--和 对象的类型有关;
构成多态--和指向对象有关
1.父类的指针或者引用;
2.调用的函数必须是虚函数的重写;
通过上述例子,对虚函数的总结如下:
1. 派生类重写基类的虚函数实现多态,要求函数名、参数列表、返回值完全相同。(协变除外,协变指返回值可以不同,返回值为父类的指针或者引用)
2. 基类中定义了虚函数,在派生类中该函数始终保持虚函数的特性。
3. 只有类的成员函数才能定义为虚函数。
4. 静态成员函数不能定义为虚函数。
5. 如果在类外定义虚函数,只能在声明函数时加virtual,类外定义函数时不能加virtual。
6. 构造函数不能为虚函数(构造函数除了初始化数据成员还会初始化虚表),虽然可以将operator=定义为虚函数,但是最好不要将operator=定义为虚函数,因为容易使用时容易引起混淆。
7. 不要在构造函数和析构函数里面调用虚函数,在构造函数和析构函数中,对象是不完整的,可能会发生未定义的行为。
8. 最好把基类的析构函数声明为虚函数,这样可以保证释放派生对象时,按正确的顺序调用析构函数(析构函数比较特殊,因为派生类的析构函数跟基类的析构函数名称不一样,但是构成覆盖,这里是因为编译器做了特殊处理,导致基类和派生类的析构函数名一致)
现在我们大概了解了多态是什么以及需要注意的一些问题,那么很好奇多态底层究竟是如何实现的呢?
这里先来举一个简单的例子:
#include<iostream> using namespace std; class Base{ public: virtual void fun1() { } private: int a; }; int main() { cout<<sizeof(Base)<<endl; Base b; system("pause"); return 0; }
猜一下,程序的运行结果是4吗?No!!运行结果为8。为什么是8呢?类Base里面明明只有一个整型成员a呀!通过监视窗口我们可以看到:
对象b里面不仅存了一个数据成员a,还存在一个指针,这个指针其实就是虚表指针(全称为虚函数表指针)。虚函数是通过一块连续的内存来存储虚函数的地址。
#include<iostream> using namespace std; class Base{ public: virtual void fun1() { cout<<"Base::fun1"<<endl; } virtual void fun2() { cout<<"Base::fun2"<<endl; } void fun3() { } private: int a; }; class Derive:public Base { public: virtual void fun1() { cout<<"Derive::fun1()"<<endl; } virtual void fun3() { cout<<"Derive::fun3()"<<endl; } }; int main() { cout<<sizeof(Base)<<endl; Base b; Derive d; Base* p=&b; p->fun2(); p->fun1(); system("pause"); return 0; }
通过监视窗口可以看到:
在子类对象的虚表中,继承了父类的虚函数,父类对象的虚表中有两个虚函数时没有问题的,子类的虚函数表fun1覆盖了父类的fun1;但是在程序中子类对象的虚表还有一个fun3函数为虚函数,却没有出现在子类的虚表中,难道它真的不存在吗?
我们可以通过一段代码打印虚表:
#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** VTable) { cout<<" 虚表地址>"<< VTable<<endl ; for (int i = 0; VTable[i ] != NULL; ++i)//虚函数表以NULL结束 { printf(" 第%d个虚函数地址 :0X%p,->", i , VTable[i ]); FUNC f = (FUNC) VTable[i ]; f();//调用对应的函数 } cout<<endl ; } void Test1 () { Base b1 ; Derive d1 ; Derive d2 ; int** VTable1 = (int**)(*( int**)&b1 );//拿到指向虚表的指针 int** VTable2 = (int**)(*( int**)&d1 );//根据平台(64位或者32位)来确定解引用一次取多少个字节 int** VTable3 = (int**)(*( int**)&d2 );//提高程序的可移植性 PrintVTable(VTable1 ); PrintVTable(VTable2 ); PrintVTable(VTable3 ); } int main() { Test1(); system("pause"); return 0; }
运行结果为:
可以看出,子类里面新增的虚函数实际上是存在虚表里的,所以有些问题通过监视窗口是看不出来的,因为有些场景下编译器进行了优化。通过上述代码和运行结果来看,同类型的对象共用一份虚函数表。
多继承下的虚函数表:
#include<iostream> using namespace std; class Base1 { public : virtual void func1() { cout<<"Base1::func1" <<endl; } virtual void func2() { cout<<"Base1::func2" <<endl; } private : int a ; }; class Base2{ public : virtual void func1() { cout<<"Base2::func1" <<endl; } virtual void func2() { cout<<"Base2::func2" <<endl; } private : int b ; }; class Derive :public Base1,public Base2 { public : virtual void func1() { cout<<"Derive::func1" <<endl; } virtual void func3() { cout<<"Derive::func3" <<endl; } virtual void func4() { cout<<"Derive::func4" <<endl; } private : int d; }; int main() { cout<<sizeof(Derive)<<endl; system("pause"); return 0; }
上述程序的运行结果为20,并不是16,因为Base1和Base2各有一个虚表指针。如图,通过监视窗口可以看到:
多继承的虚函数对象模型如下:
继承的两个基类的虚函数地址不同,没有共用一张虚表。这又有一个问题,子类Deriver中的func3和func4都是虚函数,那么这两个虚函数时放在Base1的虚表里,还是放在Base2的虚表里呢?我们还是可以向上面一样通过打印虚表来看一下,代码如下:
#include<iostream> using namespace std; class Base1 { public : virtual void func1() { cout<<"Base1::func1" <<endl; } virtual void func2() { cout<<"Base1::func2" <<endl; } private : int a ; }; class Base2{ public : virtual void func1() { cout<<"Base2::func1" <<endl; } virtual void func2() { cout<<"Base2::func2" <<endl; } private : int b ; }; class Derive :public Base1,public Base2 { public : virtual void func1() { cout<<"Derive::func1" <<endl; } virtual void func3() { cout<<"Derive::func3" <<endl; } virtual void func4() { cout<<"Derive::func4" <<endl; } private : int d ; }; 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 ; } int main() { Derive d; int* VTable1= (int*)(*( int*)&d ); PrintVTable(VTable1 ); int* VTable2= (int*)(*( int*)((char*)&d +sizeof(Base1))); PrintVTable(VTable2); system("pause"); return 0; }
运行结果为:
第一个虚表为子类Base1的虚表,第二个虚表为父类Base2的虚表,可以看出func4放在了Base1的虚表里,说明子类新增的虚函数时存在于Base1中的,一般来说,先继承哪个父类,就把新增的虚函数地址放在哪个父类的虚函数表里。
注意:并不是所有的虚函数调用都要去虚表里面去找函数的地址,只有构成多态的时候调用虚函数才会到虚表里面去找。
菱形继承
存在了多继承,那么就会存在菱形继承;
我们来通过一段代码看一下什么叫菱形继承:
#include<iostream> using namespace std; class A { public: virtual void fun1() { } public: int _a; }; class B:public A { public: virtual void fun1() { } int _b; }; class C:public A { public: virtual void fun1() { } int _c; }; class D:public B,public C { public: int _d; }; int main() { cout<<sizeof(D)<<endl; D d; d.B::_a=1; d.C::_a=2; d._b=3; d._c=4; d._d=5; system("pause"); return 0; }
经过调试,通过内存窗口我们可以看到如下的内存模型:
所以可以得出菱形继承的对象模型如下:
菱形继承的隐患:造成了数据冗余和二义性问题。
菱形虚拟继承
代码如下:
#include<iostream> using namespace std; class A { public: virtual void fun1() {} public: int _a; }; class B:virtual public A { public: virtual void fun1() {} int _b; }; class C:virtual public A { public: virtual void fun1() {} int _c; }; class D:public B,public C { public: void fun1() {} public: int _d; }; int main() { D d; d.B::_a=1; d.C::_a=2; d._b=3; d._c=4; d._d=5; system("pause"); return 0; }
通过观察内存窗口:
在单步调试过程中,可以发现最后的那个地址(0x006FF740)里面的内容开始变为1,后来又变为2,说明这个地址里面存放的是基类A里面的_a,并且因为是虚拟继承的原因,B和C里面共享一个A类数据成员。
所以我们可以得出菱形虚拟继承的对象模型如下:
那么我们把上述代码中的B类变为如下后会出现什么问题呢?
class B:virtual public A { public: virtual void fun1() {} virtual void fun3() {} int _b; };
运行调试后,我们打开内存窗口:
不难发现,B类比前面多了4个字节;我们很容易想到,B类重写了A类的fun1(),并且有了自己的虚函数fun3(),所以我们会猜测多的这4个字节会不会是B类的虚函数表指针呢?是的,没错,再来捋一捋D的这个内存结构图:
类B有两个指针,分别是虚函数表指针和虚基表指针,那前面两个指针究竟哪个是虚函数表指针,哪个是虚基表指针呢?
我们分别进入到这两个地址里面去观察一下:
第一个地址:0x0136ccc8
这就是一个普通的虚函数表,里面存放的是B类的虚函数的地址。
第二个地址:0x0136cce4
看到这里,我们就会明白,类B对象模型的这两个指针确实一个是虚函数表指针,一个是虚基表指针,并在在VS这个平台下虚函数表指针在虚基表指针的前面(其他平台不一定是这样),虚函数表里面存放的是虚函数的地址,虚基表里面存放了两个内容:分别为相对于虚函数表的偏移量和相对于公共基类的偏移量。
说到这里,我们可能还会抱有一种怀疑的态度,我们不妨通过代码再来验证一下:
#include<iostream> using namespace std; class A { public: virtual void fun1() {} virtual void fun2() {} public: int _a; }; class B:virtual public A { public: virtual void fun1() {} virtual void fun3() {} int _b; }; class C:virtual public A { public: virtual void fun1() {} virtual void fun4() {} int _c; }; class D:public B,public C { public: virtual void fun1() {} virtual void fun5() {} public: int _d; }; int main() { cout<<"D的大小为:"<<sizeof(D)<<endl; D d; d._a=1; d._b=2; d._c=3; d._d=5; system("pause"); return 0; }
想一下,如果上面分析正确,那么D的大小应该是多大?首先B类有一个虚表指针和一个虚基表指针以及自己的数据成员_b,大小为12;C类也有一个虚表指针和一个虚基表指针以及自己的数据成员_c,大小为12;D类有一个成员_d,大小为4;A类有一个虚表指针和自己的成员_a,大小为8;总大小应该为36个字节。那么到底是不是呢?
程序的运行结果为:
完全符合预期!我们再通过内存窗口观测一下对象模型:
再来一段代码打印一下上述对象模型的虚函数表:
#include<iostream> using namespace std; class A { public: virtual void fun1() { cout<<"A::fun1()"<<endl; } virtual void fun2() { cout<<"A::fun2()"<<endl; } public: int _a; }; class B:virtual public A { public: virtual void fun1() { cout<<"B::fun1()"<<endl; } virtual void fun3() { cout<<"B::fun3()"<<endl; } int _b; }; class C:virtual public A { public: virtual void fun2() { cout<<"C::fun2()"<<endl; } virtual void fun4() { cout<<"C::fun4()"<<endl; } int _c; }; class D:public B,public C { public: virtual void fun3() { cout<<"D::fun3()"<<endl; } virtual void fun5() { cout<<"D::fun5()"<<endl; } public: int _d; }; typedef void (* FUNC) (); void PrintVTable (int** VTable) { cout<<" 虚表地址>"<< VTable<<endl ; for (int i = 0; VTable[i ] != NULL; ++i) { printf(" 第%d个虚函数地址 :0X%x,->", i , VTable[i ]); FUNC f = (FUNC) VTable[i ]; f(); } cout<<endl ; } int main() { D d; d._a=1; d._b=2; d._c=3; d._d=5; PrintVTable((int**)(*((int**)&d)));//D PrintVTable((int**)((*(int**)((char*)&d+sizeof(B)-sizeof(A)))));//B PrintVTable((int**)((*(int**)((char*)&d+sizeof(D)-sizeof(A)))));//C system("pause"); return 0; }
运行结果为:
刚开始一看,看到这个运行结果还有点懵逼,这个结果怎么出来的呢?根据这个结果,我们先来画出D类的内存模型为:
这样还是不够清晰,我们来一步步进行分析:
菱形虚拟继承是多继承和虚继承的结合,B和C是从基类虚拟继承而来所以会有两个偏移量表,在继承过程中先构造D类对象,形成一个虚表.此时的虚表为:
&A::fun1() &A::fun2();
B虚继承A:fun1()构成重写,并且新增加一个虚函数fun3();形成两个虚表:
&B::fun3() 和 &B::fun1() ,&A::fun2()
C虚继承A:fun2构成重写,并且新增了虚函数fun4(),形成两个虚表:
&C:fun4() 和 &A::fun1() ,&C::fun2()
在多继承时,基类的初始化顺序按照声明顺序依次执行:所以先执行public B:在虚继承中,派生类中未构成重写的函数需要放在基类中第一个虚表的后面,所以此时的虚表变为:
&D::fun3(),&D::fun5() 和 &B::fun1(),&A::fun2()
再执行public C:派生类中构成重写的虚函数放在了B的虚表里,因为fun4()没有重写,所以fun4()的虚表不变,此时虚表为:
&B::fun1(),&A::fun2() 和 &A::fun1(),&C::fun2()
此时相对于基类构成了重写,此时两张虚表变为一张:
&B::fun1() ,&C::fun2()
所以,最终的虚表为:
&D::fun3(),&D::fun5()
&C::fun4()
&B::fun1(),&C::fun2()
来一张图更清晰的理解一下这个过程:
静态联编和动态联编
函数名联编:将源代码中的函数调用解释为执行特定的函数代码块。在C语言中,每个函数名都对应不同的函数,但在C++中,由于函数重载,编译器必须查看函数参数和函数名才能确定使用哪个函数。其实编译器可以在编译过程中完成这种联编。
静态联编:在 编译过程中的联编,又称“早期联编”。
动态联编:如果基类中有虚函数,那么具体该使用哪个函数在编译期是无法确定的,因为编译器不知道用户将使用哪种类型的对象,所以编译器必须生成能够在程序运行时选择正确的虚方法的代码。
我们一般可以这样理解:编译器对非虚方法使用静态联编。对虚方法使用动态联编。
再看一段代码:
#include<iostream> using namespace std; class Base { public : virtual void func1() { cout<<"Base::func1" <<endl; } virtual void func2() { cout<<"Base::func2" <<endl; } void display () { cout<<"display()" <<endl; } void display (int i) { cout<<"display(int i)->" <<i<< endl; } private : int a ; }; class Derive :public Base { public : virtual void func1() { cout<<"Derive::func1" <<endl; } private : int b ; }; void func (Base& b) { b.func1 (); //b.func2(); b.display (); b.display (10); } void Test1 () { Base b1 ; Derive d1 ; func(b1 ); func(d1 ); } int main() { Test1(); system("pause"); return 0; }
运行结果为:
对于下面两句代码我们转到反汇编层去看一下:
很明显可以看到对于不是虚函数的display而言,去哪调用函数是在编译期就决定的,即静态多态。而对于是虚函数的func1而言,运行时要去虚表里面去查找调用函数的地址,即动态多态。