对象的类型:
多态:
在C++语言中,基类将类型相关的函数与派生类不做改变直接继承的函数区分对待,对于某些函数,基类希望它的派生类各自定义适合自身的版本,此时基类就将这些函数声明成虚函数。
C++的多态性:
面向对象(OOP)的核心思想是多态性(polymorphism)。多态这个词源于希腊语,含义为“多种形式”,我们把 具有继承关系的多个类型成为多态类型。因为我们能使用这些类型的“多种形式”而无需在意它们的差异。引用或指针的静态类型与动态类型不同这一事实正是C++语言支持多态性的根本所在。当我们使用基类的引用或指针调用基类中定义的一个函数时,我们并不知道该函数真正作用的对象是什么类型,因为它可能是一个基类的对象也有可能是一个派生类的对象。如果该函数是虚函数,则直到运行时才会决定到底执行哪个版本,判定的依据是引用或指针所绑定的对象的真实类型。
另一方面,对非虚函数的调用是在编译时进行绑定。类似的,通过对象进行的函数调用也在编译时绑定。对象的类型是确定不变的,我们无论如何都不可能令对象的动态类型与静态类型不一致,因此,通过对象进行的函数调用将在编译时绑定到该对象所属类的函数版本上。
1.静态多态
静态链编或早绑定:编译器在编译期间完成的,编译器根据函数实参的类型(可能会进行隐式类型转换),可推断出要调用哪个函数,如果有对应的函数就调用该函数,否则出现编译错误。
2.动态多态
动态链编或晚绑定:在程序执行期间(非编译期)判断所引用对象的实际类型,根据其实际类型调相应的方法。
使用virtual关键字修饰类的成员函数时,指明该函数为虚函数,派生类需要重新实现,编译器将实现动态绑定。
动态绑定的条件:(1)必须是虚函数(任何构造函数之外的非静态函数都可以是虚函数)
(2)通过基类类型的引用或者指针调用函数
#include <iostream>
#include <string>
#include <stdlib.h>
using namespace std;
class CBase
{
public :
virtual void FunTest1( int _iTest)
{
cout <<"CBase::FunTest1()" << endl;
}
void FunTest2(int _iTest)
{
cout <<"CBase::FunTest2()" << endl;
}
virtual void FunTest3(int _iTest1)
{
cout <<"CBase::FunTest3()" << endl;
}
virtual void FunTest4( int _iTest)
{
cout <<"CBase::FunTest4()" << endl;
}
};
class CDerived :public CBase
{
public :
virtual void FunTest1(int _iTest)
{
cout <<"CDerived::FunTest1()" << endl;
}
virtual void FunTest2(int _iTest)
{
cout <<"CDerived::FunTest2()" << endl;
}
void FunTest3(int _iTest1)
{
cout <<"CDerived::FunTest3()" << endl;
}
virtual void FunTest4(int _iTest1,int _iTest2)
{
cout<<"CDerived::FunTest4()"<<endl;
}
};
int main()
{
CBase* pBase = new CDerived;
pBase-> FunTest1(0);
pBase-> FunTest2(0);
pBase-> FunTest3(0);
pBase-> FunTest4(0);
//pBase-> FunTest4(0,0);
system("pause");
return 0;
}
运行结果:
如果派生类的函数与基类同名,但是参数不同,此时,不论有无virtual关键字,基类的函数将被隐藏。
如果派生类的函数与基类同名,并且参数也相同,但是基类函数没有virtual关键字,此时,基类的函数将被隐藏。
派生类重写了基类中的虚函数,则派生类对象的虚表会替换基类中的虚函数,即基类的虚函数被派生类重写的虚函数覆盖了。
派生类先拷贝基类的虚表,若派生类没有重写虚函数,则虚表与基类的相同,若重写了虚函数,则派生类会将基类相同位置上的虚函数覆盖,再在虚表后面加上派生类自己的虚函数。(基类和派生类用的不是一张虚表)
3.纯虚函数:在成员参数的形参后面写上=0,则成员函数为纯虚函数。包含纯虚函数的类叫做抽象类(接口类),抽象类不能实例化出对象。纯虚函数在派生类中重新定义以后,派生类才能实例化出对象。
回避虚函数的机制:在某些情况下,我们希望对虚函数的调用不要进行动态绑定,而是强迫其执行虚函数的某个特定版本,使用作用域限定符可以实现这一目的。
如果一个派生类虚函数需要调用它的基类版本,但是没有使用作用域限定符,则在运行时该调用将被解析为对派生类版本自身的调用,从而导致无限递归。
总结:
1、派生类重写基类的虚函数实现多态,要求函数名、参数列表、返回值完全相同。(协变除外)(协变是指当类的虚 函数返回类型是类本身的指针或引用)
2、基类中定义了虚函数,在派生类中该函数始终保持虚函数的特性。
3、只有类的非静态成员函数才能定义为虚函数,静态成员函数不能定义为虚函数。
4、如果在类外定义虚函数,只能在声明函数时加virtual关键字,定义时不用加。
5、构造函数不能定义为虚函数,虽然可以将operator=定义为虚函数,但最好不要这么做,使用时容易混淆
6、不要在构造函数和析构函数中调用虚函数,在构造函数和析构函数中,对象是不完整的,可能会出现未定义的行为。
7、最好将基类的析构函数声明为虚函数。(析构函数比较特殊,因为派生类的析构函数跟基类的析构函数名称不一样,但是构成覆盖,这里编译器做了特殊处理)
8、虚表是所有类对象实例共用的虚表剖析,对于有虚函数的类,编译器都会维护一张虚表,对象的前四个字节就是指向虚表的指针
虚表指针(虚函数的调用):
class CBase
{
public:
CBase()
{
m_iTest = 10;
}
virtual void FunTest0()
{
cout<<"CBase::FunTest0()";
}
virtual void FunTest1()
{
cout<<"CBase::FunTest1()";
}
virtual void FunTest2()
{
cout<<"CBase::FunTest2()";
}
private:
int m_iTest;
};
class CDerived:public CBase
{
public:
virtual void FunTest4()
{
cout<<"CDerived::FunTest4()";
}
virtual void FunTest5()
{
cout<<"CDerived::FunTest5()";
}
virtual void FunTest6()
{
cout<<"CDerived::FunTest6()";
}
};
typedef void (*FUN_TEST)();
void FunTest()
{
CBase base;
cout<< "CBase vfptr:"<<endl;
for (int iIdx = 0; iIdx < 3; ++iIdx)
{
FUN_TEST funTest = (FUN_TEST)(*((int*)*(int *)&base + iIdx));
funTest();
cout<< ": "<<(int *)funTest<<endl;
}
cout<<endl;
CDerived derived;
cout<< "CDerived vfptr:"<<endl;
for (int iIdx = 0; iIdx < 6; ++iIdx)
{
FUN_TEST funTest = (FUN_TEST)(*((int*)*(int *)&derived + iIdx));
funTest();
cout<< ": "<<(int *)funTest<<endl;
}
}
int main()
{
FunTest();
system("pause");
return 0;
}