以下全部总结讲解或代码举例在VS2010编译器下进行:
一、基本概念
多态:具有多种形式或形态的情形(最初来源于希腊语,在C++中有广泛的含义)。
多态分类图:
静态多态:编译器在编译期间完成的,编译器根据函数实参的类型(可能会进行隐式类型的转换),可推断出要调用哪一个函数,如果有对应的函数就调用该函数,否则出现编译错误。举例如上图函数重载实现静态多态:
class A { public: int Add(int x,int y) { cout<<"int Add(int,int)"<<endl; return x+y; } double Add(double x,double y) { cout<<"double Add(double,double)"<<endl; return x+y; } }; void FunTest() { A a; cout<<a.Add(2,3)<<endl; cout<<a.Add(3.3,5.5)<<endl; }在类A中重载了函数Add(),在编译期间根据其对象a调用函数传参的实参类型,分别调用对应的函数:
动态多态:在程序执行期间(非编译期)判断所引用对象的实际类型,根据其实际类型调用相应的函数。(首先理解虚函数)
虚函数:在类的声明中被加上virtual关键字修饰的成员函数。虚函数可以实现动态多态,在派生类中进行重写,如:
一般的类中(没有虚函数时):
class Base { public: void FunTest1() { cout<<"Base::FunTest1()"<<endl; } private: int _b; }; class Derived:public Base { public: void FunTest1() { cout<<"Derived::FunTest1()"<<endl; } private: int _d; }; void func(Base &s) { s.FunTest1(); } void FunTest() { Base b; func(b); Derived d; func(d); }上例中Derived类公有继承Base类,两类中有函数原型相同的函数FunTest1(),在测试函数中,两次调用func函数,第一次调用func()函数,用Base类对象b做参数,并通过b调用基类Base的成员函数FunTest1(),第二次调用func()函数,用派生类Derived对象d做参数,希望通过d调用派生类的成员函数FunTest1(),但是运行结果如下:
从中可知:第二次调用func()函数时,派生类对象d并没有调用自己的成员函数FunTest1(),而是调用了基类Base的,解决这个问题办法就是虚函数,即将基类的成员函数FunTest()声明为虚函数,在前面加上virtual关键字,则派生类中的函数FunTest1()相当于对基类函数的重写,自动确定为虚函数,修改如下:
class Base { public: virtual void FunTest1() { cout<<"Base::FunTest1()"<<endl; } private: int _b; }; class Derived:public Base { public: virtual void FunTest1() { cout<<"Derived::FunTest1()"<<endl; } private: int _d; };即有虚函数时运行结果如下:
所以由此可以总结出动态多态具体概念:当一个函数如上例中func()函数那样以基类对象引用(或基类对象指针)做参数时,调用该函数时给的实际参数可以是基类的对象也可以是派生类的对象(或对象地址),这时希望在运行函数时根据实际参数的类型来决定是调用基类的还是派生类的成员函数(该函数在基类与派生类中原型相同),这种情况称为动态多态。
即实现动态绑定条件:
1.必须是虚函数
2.通过基类类型的引用或者指针调用虚函数
纯虚函数:在成员函数的形参后面写上=0.则成员函数为纯虚函数。包含纯虚函数的类叫做抽象类(接口类),抽象类不能实例出对象,纯虚函数在派生类中重新定义以后,派生类才能实例出对象。
举例:
class Base { public: virtual void FunTest1()=0 {} private: int _data; }; class Derived:public Base { public: virtual void FunTest1() { cout<<"Derived::FunTest1()"<<endl; } }; void FunTest() { //Base b;//编译错误,不允许使用抽象类类型Base的对象 Base *p=new(Derived);//指针可以 p->FunTest1(); Derived d;//派生类中函数重新定义可以实例化对象 d.FunTest1(); }类中不能做虚函数的函数:
1.构造函数:原因(1)构造函数构造对象时,由于对象还未构造成功,编译器无法知道对象实际类型,是该类本身还是该类的一个派生类,而虚函数行为是在运行期间确定实际类型的;(2)虚函数的行为依赖于虚函数表,虚函数表在构造函数中进行初始化工作,即初始化vptr,让它指向正确的虚函数表,而在构造对象期间,虚函数表还没有被初始化,无法进行。
2.内联函数:原因:其目的为在代码中直接展开,减少函数调用花费的代价,虚函数是为了继承后对象能够准确的执行自己的动作,这是不可能统一的,内联函数是在编译时展开的,虚函数在运行时才能动态的绑定函数。
3.静态成员函数:原因:它对于每个类来说只有一份代码,所有的对象都共享这一份代码,没有动态绑定的必要性。
4.友元函数:友元函数不能继承,即它更没有虚函数。
类中可以做虚函数的函数:
1.析构函数:原因:在类的继承中,如果有基类指针指向派生类,那么用基类指针delete时,如果不定义为虚函数时,派生类中派生的那部分将无法析构,容易造成内存泄漏。
2.赋值运算符重载(不建议,使用容易混淆):协变:与重写条件基本相同,唯一不同协变的返回值不同,基类返回基类指针或引用,派生类返回派生类的指针或引用。
注意:
1.重载、重写、重定义区别:
2.派生类重写基类的虚函数实现多态,要求函数名、参数列表、返回值完全相同(协变除外);
3.若类中有虚函数,并没有显示定义构造函数,编译器会自动合成默认构造函数;
4.基类中定义了虚函数,在派生类中该函数始终保持虚函数特性;
5.若在类外定义虚函数,只能在声明函数时加virtual关键字,定义时不用加;
6.不要在构造函数和析构函数中调用虚函数,在它们中,对象是不完整的,可能会出现未定义的行为;
7.虚表是所有类对象实例共用的。
二、类中虚表简单剖析
类中有虚函数时内存布局,举例如:
class Base { public: virtual void FunTest1() { cout<<"Base::FunTest1()"<<endl; } virtual void FunTest2() { cout<<"Base::FunTest2()"<<endl; } virtual void FunTest3() { cout<<"Base::FunTest3()"<<endl; } virtual void FunTest4() { cout<<"Base::FunTest4()"<<endl; } public: int _data; }; typedef void (*pvf)(); void Print() //打印虚表(执行类中虚函数),证明类对象b前四个字节为虚函数表的首地址 { Base b; b._data=1; pvf *fun=(pvf *)*(int*)&b; //取到虚表地址 while(*fun) { (*fun)(); //调用每一个虚函数 fun++; } } void FunTest() { cout<<sizeof(Base)<<endl; Print(); }查看地址:
查看运行结果:
即虚表就为对于有虚函数的类,编译器都会维护一张虚表,在里存放各个虚函数的地址,最后以0结尾,而对象的前四个字节就是指向虚表的指针。
对于继承基类派生类多态虚函数实现内存布局以及原理请点击此链接: