一:基本概念:
(1)多态性(Polymorphism)是指一个名字,多种语义;或界面相同,多种实现。
(2)重载函数是多态性的一种简单形式。
(3)C++为类体系提供一种灵活的多态机制——虚函数。
(4)虚函数允许函数调用与函数体的联系在运行时才进行,称为动态联编
二:虚函数和动态联编
(1) 冠以关键字 virtual 的成员函数称为虚函数
(2)实现运行时多态的关键首先是要说明虚函数,另外,必须用
基类指针调用派生类的不同实现版本
三:动态联编:
动态联编是指联编在程序运行时动态地进行,根据当时的情况来确定调用哪个同名函数,实际上是在运行时虚函数的实现。这种联编又称为晚期联编,或动态束定。动态联编对成员函数的选择是基于对象的类型,针对不同的对象类型将做出不同的编译结果。C++中一般情况下的联编是静态联编,但是当涉及到多态性和虚函数时应该使用动态联编。动态联编的优点是灵活性强,但效率低。
动态联编规定,只能通过指向基类的指针或基类对象的引用来调用虚函数,其格式为:指向基类的指针变量名->虚函数名(实参表)或基类对象的引用名.虚函数名(实参表)
动态联编要求派生类中的虚函数与基类中对应的虚函数具有相同的名称、相同的参数个数和相同的对应参数类型、返回值或者相同,或者都返回指针或引用,并且派生类虚函数所返回的指针或引用的基类型是基类中虚函数所返回的指针或引用的基类型的子类型。如果不满足这些条件,派生类中的虚函数将丢失其虚特性,在调用时进行静态联编。
实现动态联编需要同时满足以下三个条件:
① 必须把动态联编的行为定义为类的虚函数。
② 类之间应满足子类型关系,通常表现为一个类从另一个类公有派生而来。
③ 必须先使用基类指针指向子类型的对象,然后直接或者间接使用基类指针调用虚函数。
四:虚函数和基类指针:
当单继承且非虚继承时:每个含有虚函数的表只有一个虚函数表,所以只需要一个虚表指针即可;
当多继承且非虚继承时:一个子类有几个父类则会有几个虚函数表,所以就有和父类个数相同的虚表指针来标识;
总之,当时非虚继承时,不需要额外增加虚函数表指针。
当虚继承时:无论是单虚继承还是多虚继承,需要有一个虚基类表来记录虚继承关系,所以此时子类需要多一个虚基类表指针;而且只需要一个即可。
当虚继承时可能出现一个类中持有多个虚函数表的情况:无论是单虚继承还是多虚继承,
如果子类没有构造函数和析构函数,且子类中的虚函数都是在父类中出现的虚函数,这个时候不需要增加任何虚表指针;只需要像多继承那个持有父类个数的虚表指针来标识即可;
如果子类中含有构造函数或者析构函数或二者都有,则在子类中只要每出现一个父类中的虚函数则需要增加一个虚函数表指针来标识此类的虚函数表;
无论是否含有构造函数或者虚构函数,只要继承都是虚继承且出现了父类中没有出现的虚函数,则在子类中需要再增加一个徐函数表指针;如果其中有一个是非虚继承,则按照最省空间的原则,不需要增加虚函数表指针,因为这个时候可以和非虚基类共享一个虚函数表指针。
注意:
(1) 一个虚函数,在派生类层界面相同的重载函数都保持虚特性
(2)虚函数必须是类的成员函数
(3)不能将友元说明为虚函数,但虚函数可以是另一个类的友元
(4)析构函数可以是虚函数,但构造函数不能是虚函数
五:虚函数的重载特性:
(1)在派生类中重载基类的虚函数要求函数名,返回类型,参数个数,参数类型和顺序完全相同
(2)如果仅仅返回类型不同,C++认为是错误重载
(3)如果函数原型不同,仅函数名相同,丢失虚特性
六:虚析构函数:
虚析构函数是为了解决基类的指针指向派生类对象,并用基类的指针删除派生类对象。
如果某个类不包含虚函数,那一般是表示它将不作为一个基类来使用。当一个类不准备作为基类使用时,使析构函数为虚一般是个坏主意。因为它会为类增加一个虚函数表,使得对象的体积翻倍,还有可能降低其可移植性。
所以基本的一条是:无故的声明虚析构函数和永远不去声明一样是错误的。实际上,很多人这样总结:当且仅当类里包含至少一个虚函数的时候才去声明虚析构函数。
抽象类是准备被用做基类的,基类必须要有一个虚析构函数,纯虚函数会产生抽象类,所以方法很简单:在想要成为抽象类的类里声明一个纯虚析构函数。
虚析构函数是为了解决这样的一个问题:基类的指针指向派生类对象,并用基类的指针删除派生类对象。
如果某个类不包含虚函数,那一般是表示它将不作为一个基类来使用。当一个类不准备作为基类使用时,使析构函数为虚一般是个坏主意。因为它会为类增加一个虚函数表,使得对象的体积翻倍,还有可能降低其可移植性。
所以基本的一条是:无故的声明虚析构函数和永远不去声明一样是错误的。实际上,很多人这样总结:当且仅当类里包含至少一个虚函数的时候才去声明虚析构函数。
抽象类是准备被用做基类的,基类必须要有一个虚析构函数,纯虚函数会产生抽象类,所以方法很简单:在想要成为抽象类的类里声明一个纯虚析构函数。
例:
class awov { // awov = "abstract w/o
// virtuals"
public:
virtual ~awov() = 0; // 声明一个纯虚析构函数
}
这个类有一个纯虚函数,所以它是抽象的,而且它有一个虚析构函数,所以不会产生析构函数问题。但这里还有一件事:必须提供纯虚析构函数的定义:
awov::~awov() {} // 纯虚析构函数的定义
这个定义是必需的,因为虚析构函数工作的方式是:最底层的派生类的析构函数最先被调用,然后各个基类的析构函数被调用。这就是说,即使是抽象类,编译器也要产生对~awov的调用,所以要保证为它提供函数体。如果不这么做,链接器就会检测出来,最后还是得回去把它添上。
注意:如果声明虚析构函数为inline,将会避免调用它们时产生的开销,但编译器还是必然会在什么地方产生一个此函数的拷贝。
说明:
1.派生类应该从它的基类公有派生。
2.必须首先在基类中定义虚函数。
3.派生类对基类中声明虚函数重新定义时,关键字virtual可以不写。
4.一般通过基类指针访问虚函数时才能体现多态性。
5.一个虚函数无论被继承多少次,保持其虚函数特性。
6.虚函数必须是其所在类的成员函数,而不能是友元函数,也不能是静态函数。
7.构造函数、内联成员函数、静态成员函数不能是虚函数。
(虚函数不能以内联的方式进行处理)
8.析构函数可以是虚函数,通常声明为虚函数
七:成员函数调用虚函数(采用动态联编):
#include <iostream>
using namespace std;
class A
{
public:
A(){cout<<"A's cons."<<endl;}
virtual ~A(){cout<<"A's des."<<endl;}
virtual void f(){cout<<"A's f()"<<endl;}
void g(){f();}
};
class B:public A
{
public:
B(){f(); cout<<"B's cons."<<endl;}
~B(){cout<<"B's des."<<endl;}
};
class C:public B
{
public:
C(){cout<<"C's cons."<<endl;}
~C(){cout<<"C's des."<<endl;}
void f(){cout<<"C's f()."<<endl;}
};
void main(){
A *a = new C;
a->g();
delete a;
}
输出:
A's cons.
A's f()
B's cons.
C's cons.
C's f().
C's des.
B's des.
A's des.
构造函数和析构函数中调用虚函数采用静态联编,成员函数中调用虚函数采用动态联编。
八: 纯虚函数和抽象类:
纯虚函数在C++编程中的地位很重要,其关联到了设计模式中“接口”的概念。
纯虚函数的语法:
1、 将成员函数声明为virtual
2、 后面加上 = 0
3、 该函数没有函数体
1 class <类名>2 {3 virtual <类型><函数名>(<参数表>) = 0;4 …5 };
例如:
1 class CmdHandler2 {3 virtual void OnCommand(char* cmdline) = 0;4 …5 };
在许多情况下,在基类中不能对虚函数给出有意义有实现,而把它说明为纯虚函数,它的实现留给该基类的派生类去做。这就是纯虚函数的作用。
包含纯虚函数的类称为抽象类(Abstract Class)。之所以说它抽象,是因为它无法实例化,也就是无法创建对象。原因很明显,纯虚函数没有函数体,不是完整的函数,无法调用,也无法为其分配内存空间。
抽象类通常是作为基类,让派生类去实现纯虚函数。派生类必须实现纯虚函数才能被实例化。
关于纯虚函数的几点说明
1) 一个纯虚函数就可以使类成为抽象基类,但是抽象基类中除了包含纯虚函数外,还可以包含其它的成员函数(虚函数或普通函数)和成员变量。
2) 只有类中的虚函数才能被声明为纯虚函数,普通成员函数和顶层函数均不能声明为纯虚函数。
抽象类:
含有纯虚函数的类叫做抽象类(纯虚类),抽象类是一种特殊的类,它是为了抽象和设计的目的而建立的,它处于继承层次结构的较上层。
抽象类不能被实例化,即无法创建该类的对象。
CmdHandler ch; // 编译错误!!
CmdHandler *p = new CmdHandler(); // 编译错误!!
在实际中为了强调一个类是抽象类,可将该类的构造函数说明为保护的访问控制权限。
抽象类的主要作用是将有关的组织在一个继承层次结构中,由它来为它们提供一个公共的根,相关的子类是从这个根派生出来的。
抽象类刻画了一组子类的操作接口的通用语义,这些语义也传给子类。一般而言,抽象类只描述这组子类共同的操作接口,而完整的实现留给子类。
抽象类只能作为基类来使用,其纯虚函数的实现由派生类给出。如果派生类没有重新定义纯虚函数,而派生类只是继承基类的纯虚函数,则这个派生类仍然还是一个抽象类。如果派生类中给出了基类纯虚函数的实现,则该派生类就不再是抽象类了,它是一个可以建立对象的具体类了。
接口:
实际用途:充当“接口函数”
(相当于Java中的interface语法)
(用于替代C中的回调函数的用法)
接口规范:凡是遵循此规范的类,都必须实现指定的函数接口,通常是一系列接口。
上述定义的抽象类可以理解为:凡是遵循CmdHandler规范的类,都必须实现指定的函数接口:OnCommand()。