c++多态性分为两类:静态多态性和动态多态性。静态多态性又称编译时多态性,是通过函数重载实现的,特点是函数调用速度快、效率高,但缺乏灵活性,在程序运行前就已经决定了执行的函数和方法。动态多态性又称运行时多态性,是通过虚函数(virtual function)实现的,特点是不在编译时确定调用的是哪个函数,而是在程序运行过程中才动态地确定操作所针对的对象。
#include "StdAfx.h"
#include <iostream>
#include <string>
using namespace std;
class Student
{
public:
Student(int,string,float);
void fnDisply();
protected:
int m_iNum;
string m_sName;
float m_fScore;
};
Student::Student(int iNum,string sName,float fScore)
{
m_iNum = iNum;
m_sName = sName;
m_fScore = fScore;
}
void Student::fnDisply()
{
cout<<"m_iNum:"<<m_iNum<<"\nm_sName:"<<m_sName<<"\nm_fScore:"<<m_fScore<<"\n\n";
}
class Graduate:public Student
{
public:
Graduate(int,string,float,float);
void fnDisply();
private:
float m_fWage;
};
Graduate::Graduate(int iNum,string sName,float fScore,float fWage):Student(iNum,sName,fScore),m_fWage(fWage)
{
}
void Graduate::fnDisply()
{
cout<<"m_iNum:"<<m_iNum<<"\nm_sName:"<<m_sName<<"\nm_fScore:"<<m_fScore<<"\nm_fWage = "<<m_fWage<<endl;
}
int main()
{
Student Stud1(1001,"xiao ming",88.5);
Graduate Grad1(2001,"li dong",98.5,1200);
Student *pt = &Stud1;
pt->fnDisply();
pt = &Grad1;
pt->fnDisply();
return 0;
}
如果在基类Student中声明fnDisplay()函数时,加上virtual关键字,使它成为虚函数,程序其它部分不动,再编译和运行,结构:
virtual void fnDisply();
定义了虚函数则隐含着:这个类会被继承,并且会通过基类的指针指向子类对象,从而得到多态性。核心理念就是通过基类访问派生类定义的函数。
编译器是如何针对虚函数产生可以再运行时刻确定被调用函数的代码呢?也就是说,虚函数实际上是如何被编译器处理的呢?Lippman在深度探索C++对象模型[1]中的不同章节讲到了几种方式,这里把“标准的”方式简单介绍一下。
我所说的“标准”方式,也就是所谓的“VTABLE”机制。编译器发现一个类中有被声明为virtual的函数,就会为其搞一个虚函数表,也就是VTABLE。VTABLE实际上是一个函数指针的数组,每个虚函数占用这个数组的一个slot。一个类只有一个VTABLE,不管它有多少个实例。派生类有自己的VTABLE,但是派生类的VTABLE与基类的VTABLE有相同的函数排列顺序,同名的虚函数被放在两个数组的相同位置上。在创建类实例的时候,编译器还会在每个实例的内存布局中增加一个vptr字段,该字段指向本类的VTABLE。通过这些手段,编译器在看到一个虚函数调用的时候,就会将这个调用改写:
class A
{
public:
virtual void foo();
};
class B: public A
{
virtual void foo();
};
void bar(A * a)
{
a->foo();
}
//会被改写为:
void bar(A * a)
{
a->vptr[1])();
}
因为派生类和基类的foo()函数具有相同的VTABLE索引,而他们的vptr又指向不同的VTABLE,因此通过这样的方法可以在运行时刻决定调用哪个foo()函数。一个类只有一个VTABLE,所以基类和派生类具有自己的VTABLE(函数指针)。但两者有相同的虚函数排列顺序。同名的虚函数被放在VTABLE表的相同位置。
在创建类实例时,编译器会在类的内存空间生成一个vptr字段,指向该类的VTABLE.r /> 当调用上面的foo函数时,根据A *a=new A 还是 A *a=new B决定调用哪个类的foo函数,从而实现多态。
虚函数的使用方法:
1、在基类中用virtual声明成员函数为虚函数。在类外定义虚函数时,不必再加virtual。
2、在派生类中重新定义次函数,函数名、函数类型、函数参数个数和类型必须与基类的虚函数相同,根据派生类的需要重新定义函数体。
当一个成员函数被声明为虚函数后,其派生类中的同名函数都自动成为虚函数。因此在派生类重新声明该虚函数时,可以加virtual,也可以不加,但习惯上一般在每一层声明该函数时都加virtual,使程序更加清晰。
如果在派生类中没有对基类的虚函数重新定义,则派生类简单地继承其直接基类的虚函数。
3、定义一个指向基类对象的指针变量,并使它指向同一类族需要调用该函数的对象。
4、通过该指针变量调用此虚函数,此时调用的就是指针变量指向的对象的同名函数。
虚析构函数。类如果会被派生的话,析构函数一般都应该定义为virtual的,主要不是防止内存泄露,而是为了正确的析构。如果是个封闭类(即不再被派生),就不要定义为virtual的。虚函数毕竟耗费较大的。
struct A
{
A(){}
~A() {cout<<"~A()\n";}
//virtual ~A() {cout<<"~A()\n";}
};
struct B: public A
{
B(){}
~B() {cout<<"~B()\n";}
};
int main()
{
A* pt = new B; //多态出现
delete pt;
return 0;
}
表示只执行了基类A的析构函数,而没有执行派生类B的析构函数。原因前面介绍过。如果希望能执行派生类B的析构函数,就将基类的析构函数声明为虚析构函数。
virtual ~A() {cout<<"~A()\n";}
当基类的析构函数为虚析构函数时,无论指针指的是同一类族中的哪一个对象,系统都会采用动态关联,调用相应类的析构函数,对该对象进行清理工作。
最好把基类的析构函数声明为虚析构函数。这将使所有派生类的析构函数自动成为虚析构函数。这样,如果程序中显式地用了delete运算符准备删除一个对象,而delete运算符的操作对象用了指向派生类对象的基类指针,则系统会调用相应类的析构函数。
专业人员把基类的析构函数声明为虚析构函数。即使基类并不需要析构函数,也显式地定义一个函数体为空的虚析构函数,以保证在撤销动态分配空间时能得到正确的处理。
构造函数不能声明为虚函数。
纯虚函数。virtual 函数类型 函数名(参数列表) = 0;
class A
{
public:
virtual void foo()=0; }
一个函数声明为纯虚作用是在基类中为派生类保留一个函数名字,以便派生类根据需要对它进行定义:我是一个抽象类!不要把我实例化!不能被实例化,只能被子类覆盖。纯虚函数用来规范派生类的行为,实际上就是所谓的“接口”。它告诉使用者,我的派生类都会有这个函数。