多态性的基本概念
顾名思义,多态的意思是在接收到同一种消息或者调用的情况下,不同事物表现出来的多种形态。比如,现实世界中当警车开过,警报声响起时,不同的人会表现出不同的反应:一般人没什么太大反应,但是如果一个在逃的罪犯他的反应就比较强烈,甚至胆战心惊。
从程序设计的角度看,多态性通常指对同一个消息、同一种调用,在不同的场合,不同的情况下,执行不同的行为。具体来讲就是:无论发送消息的对象属于什么类,它们均发送具有同一形式的消息,对消息的处理方式可能随接收消息的对象而变。这种处理方式被称为多态性。
在C++中,多态是通过虚函数来实现的。
虚函数的定义和使用
虚函数就是在基类中使用关键词virtual修饰,并在派生类中重新定义功能的类的成员函数。或者说虚函数是在基类中使用关键词virtual来说明,并在该类的派生类中被重新定义并被赋予另外一种处理功能的成员函数。
虚函数定义的一般形式为:
class基类名
{
private:
virtual myfun(){//基类中的虚函数函数体}
};
class派生类名:基类名
{
private:
//派生类中该函数定义时,关键词virtual可以省略,也即默认该函数就是虚函数
//如果再一次进行派生的话依此类推
virtual myfun(){//派生中的虚函数函数体}
};
如果类中包含有虚成员函数,在用该类实例化对象时,对象的第一个成员将是一个指向虚函数表的指针(pvftable)。虚函数表记录运行过程中实际应该调用的虚函数的入口地址。如下图。
(虚函数pvftable指针图例)
所以,带有虚函数的类,该类实例化的对象除了成员变量所占用的存储空间以外,还多了一个指向虚函数表的指针成员变量所占用的存储空间。
例1.演示带有虚函数的类对象存储于所需空间大小
#include<iostream>
usingnamespace std;
classCtest
{
private:
int m_iCount;
public:
virtual void PublicFun()
{
cout<<m_iCount<<endl;
}
};
intmain()
{
Ctest test;
cout<<sizeof(Ctest)<<sizeof(test)<<endl;
return 0;
}
程序输出的值是8而不是4,就是在整型成员变量占用4个字节,而指向虚函数表的指针占用4个字节。
例2. 演示具有虚函数的多个基类派生出的派生类存储空间占用情况
#include<iostream>
usingnamespace std;
classCBaseA
{
public:
virtual void Fun()
{
cout<<"CBaseA::Fun()"<<endl;
}
};
classCBaseB
{
public:
virtual void Fun()
{
cout<<"CBaseB::Fun()"<<endl;
}
};
classCBaseC
{
public:
virtual void Fun()
{
cout<<"CBaseC::Fun()"<<endl;
}
};
classCDrivedD:public CBaseA,public CBaseB,public CBaseC
{
};
intmain()
{
CDrivedD derive;
cout<<sizeof(CDrivedD)<<endl;
return 0;
}
程序输出12,因为该派生类是由三个具有虚函数的基类共同派生而来,那么该派生类就有了三个指向虚函数表的指针,所以占用空间是12。
通过以上的讨论和程序举例我们知道:在基类用virtual声明的成员函数即为虚函数。
在派生类中重新定义此函数,要求函数名、函数类型、函数参数个数和类型全部与基类的虚函数相同,并根据派生类的需要重新定义函数体。
当一个成员函数声明为虚函数后,其派生类中的同名函数都自动成为虚函数,无论是否加virtual关键字。下面举例说明使用虚函数这么来实现多态性。通过以前章节的学习我们知道,派生类对象和基类对象之间存在赋值兼容性。具体体现在可以使用指向基类对象的指针指向派生类对象,可以使用派生类对象给基类对象赋值,也可以使用派生类对象给及类对象的引用赋值。这些赋值关系也乐意体现在函数调用过程中。当一个函数的形式参数是一个指向基类对象的指针,我们可以将一个派生类对象的地址作为实际参数来调用该函数。如果一个函数的形参是一个基类对象,那么我们可以使用一个派生类对象作为实际参数来对该函数进行调用。同理如果一个函数的的形参是及类对象的引用,可以使用派生类对象作为实际参数来调用该函数。但是注意一点,反过来就不能成立。比如不能将一个基类对象给派生类对象赋值。
所以,用基类的指针或者引用指向派生类的对象,通过用基类的指针或者引用调用虚函数,实际执行的将是派生类对象中定义的虚函数。
例3. 演示类的成员函数访问同类的成员
#include<iostream>
usingnamespace std;
classCWorm
{
protected:
void *m_hImage;
bool LoadBmp(const char *pszBmpName)
{
//m_hImage=::LoadBitmap(NULL,pszBmpName);
return m_hImage!=NULL;
}
public:
virtual void Draw() //声明虚函数
{
cout<<"CWorm::Draw()"<<endl;
}
};
classCAnt:public CWorm
{
public:
void Draw() //在派生类中重新定义函数功能
{
cout<<"CAnt::Draw()"<<endl;
}
};
classCSpider:public CWorm
{
public:
void Draw() //在派生类中重新定义函数功能
{
cout<<"CSpider::Draw()"<<endl;
}
};
voidmain()
{
CWorm worm;
CWorm*pWorm=&worm;//指向基类对象的指针
//还没有指向派生类对象之前,调用的自然是基类中的虚函数pWorm->Draw();
CAnt ant;
CSpider spider;
pWorm=&ant; //使用指向及类对象的指针指向派生类对象
pWorm->Draw(); //实际执行的将是派生类对象中的虚函数
pWorm=&spider; //使用指向及类对象的指针指向派生类对象
pWorm->Draw(); //实际执行的将是派生类对象中的虚函数
}
根据程序的输出联系前面的多态性概念,对同一种调用pWorm->Draw(),当指针pWorm指向不同的对象时,将执行不同的操作:指向基类对象时调用的就是基类中的虚函数,当指向派生类对象时,调用具体的派生类中的虚函数。
上例中如果去掉基类中函数前面的virtual关键词,查看程序的执行结果。理解多态性作用。
静态关联和动态关联概念
在多态性中,如果程序在编译阶段就能确定实际的执行动作,则称为静态关联(static binding)或早绑定(early binding),如果只有等到程序运行阶段才能确定实际的执行动作,则成为动态关联(dynamic binding)或晚绑定(late binding)。
在虚函数的调用中,如果通过基类的指针或者引用调用,则为动态关联,否则为静态关联。
例如上例代码:
voidmain()
{
CWorm *pWorm;
CAnt ant;
CSpider spider;
pWorm=&ant; //动态关联
pWorm->Draw();
pWorm=&spider; //动态关联
pWorm->Draw();
CWorm &wormAlias=ant; //动态关联
wormAlias.Draw();
CWorm worm;
worm=ant; //静态关联
worm.Draw();
}
虚析构函数的用法和作用
首先来看一个例子。
例4. 普通析构函数例子,释放对象不彻底
classCWorm
{
public:
virtual void Draw()
{
cout<<"CWorm::Draw()"<<endl;
}
~CWorm()
{
cout<<"CWorm::~CWorm()"<<endl;
}
};
classCAnt:public CWorm
{
public:
void Draw()
{
//如果希望在执行自己的功能的同时,也执行基类的功能,可用此方式调用
//CWorm::Draw();
cout<<"CAnt::Draw()"<<endl;
}
~CAnt()
{
cout<<"CAnt::~CAnt()"<<endl;
}
};
voidmain()
{
CWorm *pWorm=new CAnt;
//试图通过指向及类对象的指针,释放掉它所指向new开辟出来的派生类对象
pWorm->Draw();
delete pWorm;
}
观察程序的输出情况:程序只是掉用了基类的析构函数,而没有调用派生类对象的析构函数。也就是说析构得不彻底。严重时候将导致内存泄露!解决办法:引入虚析构函数。
例5. 虚析构函数例子,释放对象彻底
classCWorm
{
public:
virtual void Draw()
{
cout<<"CWorm::Draw()"<<endl;
}
virtual ~CWorm()
{
cout<<"CWorm::~CWorm()"<<endl;
}
};
classCAnt:public CWorm
{
public:
void Draw()
{
//如果希望在执行自己的功能的同时,也执行基类的功能,可用此方式调用
//CWorm::Draw();
cout<<"CAnt::Draw()"<<endl;
}
//virtual关键词可加也可不加
~CAnt()
{
cout<<"CAnt::~CAnt()"<<endl;
}
};
voidmain()
{
CWorm *pWorm=new CAnt;
//试图通过指向及类对象的指针,释放掉它所指向new开辟出来的派生类对象
pWorm->Draw();
delete pWorm;
}
观察程序输出结果:调用了派生类的析构函数,释放彻底,问题解决!
纯虚函数和抽象类
以上描述并举例说明虚函数的概念和用法。现在来看一下什么是纯虚函数。当一个函数使用如下的声明方式来声明,我们就认为该函数是纯虚函数:
纯虚函数一般声明形式如下:
virtual返回类型 函数名(参数列表)=0;
其中,也使用了关键词virtual,除此以外,后面多了=0这一部分。注意它并不是说明函数的返回值为零,而是从形式上告诉编译系统该函数是纯虚函数。它没有函数体,具体的函数实现在派生类中来进行定义。具有纯虚函数的来叫做抽象类。抽象类不能像普通类那样实例化自己。
通常在基类中声明纯虚函数,在派生类中定义该虚函数,如果派生类中也没有定义该虚函数,则该函数在派生类中仍然为虚函数。比如形状类(shape)
中声明一个纯虚函数virtual float Area()=0,只是声明,没有实现,因为现在是什么形状还没有确定下来,也就不能计算它的面积。只有形状确定下来(圆是形状的派生类),那么怎么求它的面积也就可以定下来了(在派生类中定义该纯虚函数的函数体)。
虽然抽象类不能实例化对象,但是可以用抽象类的指针指向派生类对象,并调用派生类的虚函数的实际实现。在抽象类中声明的纯虚函数只是说明了使用
该抽象类派生出来的派生类中都提供这样的接口。比如说:所有规则的形状都可以求面积,也就是都提供一个这样的接口Area来求面积。
例6. 从设计层面理解用虚函数做接口
游戏基础类库项目开发组
classCAnimal
{
public:
//因为基类对象没什么实际意义,所以
//可以通过声明Say()为纯虚函数,使
//CAnimal变为抽象类
virtual void Say()=0;
};
classCFrog:public CAnimal
{
public:
virtual void Say()
{
puts("Hi! I'm frog.");
}
};
classCSnake:public CAnimal
{
public:
virtual void Say()
{
puts("Hei hei! I'm snake.");
}
};
//游戏集成项目开发组
voidCaller(CAnimal *pAnimal)
{
pAnimal->Say();
}
intmain(int argc, char* argv[])
{
CFrog fg;
CSnake sk;
Caller(&fg);
Caller(&sk);
//CAnimal wm; //具有纯虚函数的抽象类不能实例化对象
//Caller(&wm); //没什么实际意义
return 0;
}
基类函数设计的一般原则
基类在将来往往供许多的派生类继承,为了避免过多冗余和空间浪费,一般只会将预计绝大部分派生类都具有的成员函数提取到基类中。
如果确定基类的成员函数能够实现确定的、不变的功能,则不必把该函数声明为虚的,这种成员函数的目的在于供派生类继承基类已经实现的功能。即复用。
如果预计不同的派生类都具有此成员函数的功能,但是在不同的派生类中有不同的具体实现,则应该把此种基类的成员函数声明为虚的。这种成员函数的目的并不是供派生类继承基类的功能,而是将此功能暴露给外部的调用者,外部的调用者只需要简单地知道基类函数的调用方式和大体的功能,而不必关心该功能在不同的派生类中的实际实现。亦即向外部调用者提供接口的功能。
如果有较成熟的类设计体系,或者打算用基类向外部调用者描述模块的功能,通过基类供外部调用的形式实现该模块的功能,则可以考虑将基类声明为具有纯虚函数的抽象类。
几种抽象类
该类的构造函数访问属性是私有的;
该类的构造函数访问属性是保护性的;
本章讲解的抽象类----带有纯虚函数的类;考虑并实现前两种抽象类的实例化(提示:第一种是用静态成员函数来实例化,第二种在派生类中来进行实例化)。
多态性:具体来讲就是不同的对象在接收到同一消息,同一种函数调用的时,表现出来的不同表现。在基类中使用关键字virtual来修饰一个函数,再派生类中可以对该函数定义新的函数体,让该函数随着不同的调用而执行不同的功能。如果在编译阶段就知道该调用那个函数,比如函数的重载,就称之为静态关联。如果只有在程序运行的时候才能确定调用哪一个函数,这种情况称之为动态关联。如虚函数实现的多态性。如果一个函数被声明为虚函数,并且没有函数体,直接在函数首部的后面赋值为零。这样的函数叫做纯虚函数。如果一个类具有一个以上的纯虚函数,那么着该类不能实例化自己的对象。这样的类也叫做抽象类。