12.3 虚基类

虚基类也被称为抽象类,既然是抽象事物,就不存在实体。如平常所说的东西,它就不能被实例化。将某一物品描述为东西,等同于没有描述。

在生活中,我们会经常遇到此类情况。例如,在你的书桌上有钢笔一支、水杯一个、书一本,这时你的同桌突然对你说:“把你桌子上的那个东西借我一下”,由于没有具体的描述,你无法知道他所指的“那个东西”到底是哪一件物品。

在编码过程中,虚基类的定义需要配合虚函数使用。在虚函数的声明结尾处添加“=0”,这种虚函数被称为纯虚函数。纯虚函数是一个没有实现只有声明的函数,它的存在就是为了让类具有虚基类的功能,让继承自虚基类的子类都具有虚表以及虚表指针。在使用过程中,利用虚基类指针可以更好地完成多态的工作。多态的实现分析已经在前面介绍过,而这个纯虚函数是如何实现的呢?对于一个没有实现的函数,编译器又是如何处理的呢?关于纯虚函数的分析如代码清单12-12所示。

代码清单12-12 纯虚函数的分析—Debug版


//C++源码说明:定义虚基类和派生类

class CVirtualBase{

public:

virtual void Show()=0;//定义纯虚函数

};

class CVirtualChild:public CVirtualBase{//定义继承虚基类的子类

public:

virtual void Show(){//实现纯虚函数

printf("虚基类分析\r\n");

}

};

void main(int argc, char*argv[]){

CVirtualChild VirtualChild;

VirtualChild.Show();

}

//反汇编代码分析,跟踪到虚基类构造函数中,查看其虚表信息

CVirtualBase:CVirtualBase://虚基类构造函数

00401829 pop

ecx

0040182A mov

dword ptr[ebp-4],ecx

0040182D mov

eax, dword ptr[ebp-4]

;设置虚基类虚表指针,虚表地址在0x00425068处,虚表信息如图12-10所示

00401830 mov

dword ptr[eax],offset CVirtualBase:'vftable'(00425068)

00401836 mov

eax, dword ptr[ebp-4]

0040183F ret

;虚基类CVirtualBase中虚表信息的第一项所指向的函数首地址

void__cdecl_purecall(void){

00401E90 push

ebp

00401E91 mov

ebp, esp

_amsg_exit(_RT_PUREVIRT);

00401E93 push

19h

;压入错误编码

00401E95 call

_amsg_exit(00401fd0)

;结束程序

00401E9A add esp,4

}

00401E9D pop ebp

00401E9E ret


图 12-10 虚基类CVirtualBase的虚表信息

如代码清单12-12所示,在虚基类CVirtualBase的虚表信息中,由于纯虚函数没有实现代码,因此没有首地址。编译器为了防止误调用纯虚函数,将虚表中保存的纯虚函数的首地址项替换成函数_purecall,用于结束程序,并发出错误编码信息0x19。

根据这一特性,在分析过程中,一旦在虚表中发现函数地址为_purecall函数的地址时,我们就可以高度怀疑此虚表对应的类是一个虚基类。当虚基类中定义了多个纯虚函数时,虚表中将保存相同的函数指针。在代码清单12-12中,插入新的纯虚函数,并在子类中予以实现。经过编译后,再次查看虚表信息,如图12-11所示。

图 12-11 存在多个纯虚函数的类虚表信息

在Release版下,编译器会进行优化,纯虚函数将会被优化掉。