一、多态的概念
多态就是多种形态,C++的多态分为静态多态与动态多态:
静态多态就是重载,因为在编译期决议确定,所以称为静态多态。在编译时就可以确定函数地址。
动态多态就是通过继承,然后重写基类的虚函数,来实现的多态,因为实在运行时才决议确定被调用函数,所以称为动态多态。运行时在虚函数表中寻找调用函数的地址。
在基类的函数前加上virtual关键字,在派生类中重写该函数,运行时将会根据对象的实际类型来调用相应的函数。如果对象类型是子类,就调用子类的同名函数;如果对象类型是父类,就调用父类的函数,这就是多态的表现。
class Person { public : virtual void BuyTickets() { cout<<" 买票"<< endl; } protected : string _name ; // 姓名 }; class Student : public Person { public : virtual void BuyTickets() { cout<<" 买票-半价 "<<endl ; } protected : int _num ; //学号 }; //void Fun(Person* p) void Fun (Person& p) { p.BuyTickets (); } void Test () { Person p ; Student s ; Fun(p ); Fun(s ); }
二、多态的实现原理
1. 用virtual关键字声明的函数叫做虚函数,虚函数肯定是类的成员函数。
2. 当类中声明虚函数时,编译器会在类中生成一个虚函数表。
3. 由此类生成的对象都有一个指向虚表的虚函数指针。虚表是和类对应,虚函数指针和具体对象对应。
4. 虚函数表是一个存储类virtual成员函数指针的一维数据结构,编译器自动生成与维护的。
5. 虚函数指针一般作为类对象的第一个成员。
三、探索虚函数表
虚函数表是通过一块连续的内存来存储虚函数的地址。这张表解决了继承、虚函数(重写)的问题。在有虚函数的对象实例中都存在这样一张虚函数表,它就像一张地图,指向了实际调用的虚函数。
例:
class Base { public : virtual void func1(){} virtual void func2(){} private : int a ; }; void Test1 () { Base b1; }
1、单继承
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 ] != 0; ++i) { printf(" 第%d个虚函数地址 :0X%x,->", i , VTable[i ]); FUNC f = (FUNC) VTable[i ]; f(); } cout<<endl ; } void Test1 () { Base b1 ; Derive d1 ; int* VTable1 = (int*)(*( int*)&b1 ); int* VTable2 = (int*)(*( int*)&d1 ); PrintVTable(VTable1 ); PrintVTable(VTable2 ); }
从上图可以看出,派生类Derive::func1重写基类Base::func1,且覆盖了派生类相应虚表位置上的基类的同名函数。
2、多继承
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 ); }
从上图可以看出:
子类会继承每一个直接的父类的虚函数表指针
子类重写的虚函数,其虚函数指针会覆盖对应父类中的虚函数指针
子类新定义的虚函数,其虚函数指针会按照声明或定义的顺序依次存放在继承的第一个直接父类的虚表末尾
4、常见问题
1、为什么调用普通函数比调用虚函数的效率高?
因为普通函数是静态联编的,而调用虚函数是动态联编的( 联编:程序调用函数,编译器决定使用哪个可执行代码块)。
静态联编 :在编译的时候就确定了函数的地址,然后call就调用了。
动态联编 : 首先需要取到对象的首地址,然后再解引用取到虚函数表的首地址后,再加上偏移量才能找到要调的虚函数,然后call调用。
明显动态联编要比静态联编做的操作多,肯定就费时间。
2、为什么要用虚函数表?
实现多态,父类对象的指针指向父类对象调用的是父类的虚函数,父类对象的指针指向子类调用的是子类的虚函数。
3、为什么要把基类的析构函数定义为虚函数?
在父类对象的指针指向子类时,防止执行该指针的析构函数时,函数只是只是执行父类的析构函数,而不执行派生类的析构函数。造成内存泄漏