C++中 虚函数及包含多态的实现

来源:互联网 发布:单片机控制舵机程序 编辑:程序博客网 时间:2024/06/07 14:18

我们分三个方面来说明虚函数以及用虚函数实现的包含多态。

第一个:什么是虚函数?

从语法上来说虚函数就是用virtual声明的函数。所以定义一个虚函数很简单。重点是你需要知道我们如何用虚函数解决实际的问题。

第二个:编译器是如何解析函数调用语句的?

通常我们是用一个类型定义一个对象,或者new一个对象,然后用这个类型的指针指向它,然后用对象或者指针来调用它所拥有的函数。某些时候(其实是经常)我们会遇到在子类中覆盖父类方法的情况,根据我们前面所说的指针赋值兼容性规则,我们用下面的例子详细说明一下这种语法情况,然后和后面的多态进行对比:

class A

{

public:

voidfun()

{

cout << "Ahello" << endl;

}

};

 

 

class B : public A

{

public:

voidfun()

{

cout << "Bhello" << endl;

}

};

 

int _tmain(int argc,_TCHAR* argv[])

{

//现在主要说明赋值兼容性规则

Aa;

a.fun(); // Ahello

Bb;

b.fun(); // Bhello 

//以上代码不会有任何问题或歧义

 

Bb1;

b1.fun(); // Bhello 

A a1 = b1; //用子类对象给父类对象赋值

a1.fun(); // Ahello

 

Bb2;

b2.fun(); // Bhello 

 

A &a2 = b2; //用子类对象给一个父类对象的引用赋值

a2.fun(); // Ahello

 

Bb3;

b3.fun(); // Bhello 

A *pa3 = &b3; //用子类对象地址给父类对象的指针赋值

pa3->fun(); // Ahello

 

B*pb4 = new B();

pb4->fun(); // Bhello

A *pa4 = pb4;//用子类的指针给父类的指针赋值。

pa4->fun();// Ahello

 

return0;

}

仔细观察一个,编译器在编译一个方法调用语句时,总是根据调用方法的对象或者指针的类型来调用对应的方法。那么就引出了我们的第三个问题。

第三:我们如何根据父类的指针来调用子类的方法呢?

答案就是我们前面所说的虚函数。

下面我们用实际的代码来看一下虚函数的作用,以及其内存模型。

class A

{

public:

virtualvoid fun()

{

cout << "Ahello" << endl;

}

};

 

 

class B : public A

{

public:

virtualvoid fun()

{

cout << "Bhello" << endl;

}

};

 

int _tmain(int argc,_TCHAR* argv[])

{

//现在主要说明赋值兼容性规则

Aa;

a.fun(); // Ahello

Bb;

b.fun(); // Bhello 

//以上代码不会有任何问题或歧义

 

Bb1;

b1.fun(); // Bhello 

A a1 = b1; //

a1.fun(); // Ahello

 

Bb2;

b2.fun(); // Bhello 

 

A&a2 = b2;

a2.fun();// Bhello

 

Bb3;

b3.fun(); // Bhello 

A*pa3 = &b3;

pa3->fun();// Bhello

 

B*pb4 = new B();

pb4->fun(); // Bhello

A*pa4 = pb4;

pa4->fun();// Bhello

 

return0;

}

第一种情况,用B的对象给A的对象赋值,实际上是调用了A的拷贝构造函数,也就是用b1A的部分去给a1赋值,所以我们可以认为B的对象中包含一个A对象(可能这就是为什么叫做包含多态吧)。此时用a1调用fun()就确实是用A的对象来调用fun()方法

其他三种情况a2a3a4实际上表示或者指向的都是一个B类型的对象,所有后面输出的就都是“Bhello

 

为了弄清楚程序究竟是如何工作的,我们从反汇编和内存的角度来看一个它的内存模型。

在前面所说的对象的内存模型中,我们知道程序在运行时会先把方法代码载入到代码区,然后把方法的入口地址以jmp地址的方式给出方法入口表。而我们在代码中对方法的调用会被编译器自动转换为call入口表中对应入口地址

的方式。

 

现在再来看一下含有虚函数的情况:

pb4->fun(); // Bhello

00FE6A91  mov         eax,dword ptr [pb4] //1- 把对象的首地址复制到eax

00FE6A94  mov         edx,dword ptr [eax] // 2-把以eax的内容为地址的4个字节的内存空间的内容复制到edx,实际上它就是虚指针。

00FE6A96  mov        esi,esp 

00FE6A98  mov         ecx,dword ptr [pb4]  // ecx存放对应对象的首地址

00FE6A9B  mov         eax,dword ptr [edx] // 3-然后把以edx的内容为地址的内存空间的内容复制到eax,现在eax就是函数入口表中对应的函数入口地址了。

00FE6A9D  call        eax //4-调用,跳转

00FE6A9F  cmp        esi,esp 

00FE6AA1  call       __RTC_CheckEsp (0FE136Bh) 

A *pa4 = pb4;

00FE6AA6  mov        eax,dword ptr [pb4] 

00FE6AA9  mov        dword ptr [pa4],eax 

pa4->fun();// Bhello

00FE6AAC  mov        eax,dword ptr [pa4] 

00FE6AAF  mov        edx,dword ptr [eax] 

00FE6AB1  mov        esi,esp 

00FE6AB3  mov        ecx,dword ptr [pa4] 

00FE6AB6  mov        eax,dword ptr [edx] 

00FE6AB8  call       eax 

如果你看完上面加粗的说明之后还没有头晕,那么恭喜你,你不是正常人^^

作为正常人,我需要去看看内存中的具体数据。

第一步:mov  eax, dword ptr [pb4]      [pb4] == pb4 =0x0061D7E0,执行之后eax =0x0061D7E0

它所对应的前四个字节的内容为0x00FED998

第二步:mov edx,dword ptr [eax] ; [eax] =0x0061D7E0为地址的内存单元的内容,也就是0x00FED998

也就是虚指针,此时edx = 0x00FED998

我们看一下0x00FED998地址处的内容:

可以看到,此处的内容都是00fe开头的一些内存空间的地址。

 

第三步:moveax,dword ptr [edx]; [edx] =0x00FED998为首地址的内存单元的内容,也就是0x00FE151E

此时,EAX = 00FE151E

 

第四步:call eax;call后面,说明eax保存的是可执行代码了,所以我们查看一下此处的反汇编

A::A:

00FE1519  jmp        A::A (0FE68F0h) 

B::fun:

00FE151E jmp         B::fun (0FE2DD0h) 

A::fun:

00FE1523  jmp        A::fun (0FE2D70h) 

B::B:

00FE1528  jmp        B::B (0FE98F0h) 

A::A:

00FE152D  jmp        A::A (0FE6950h) 

可以看到,从call开始,进入到普通函数调用的函数入口表中的函数入口地址。

(ps:虚表和函数入口表是在一段内存空间中存放)

 

总结一下,对于含有虚函数的类,在生成对象时,会在对象的前四个字节保存一个虚指针,我们称虚指针指向的内存空间是一个虚表,它是由一系列虚函数的入口的地址组成的,然后用call进行程序的跳转,回到普通函数的调用方法上。

 

对应的我们可以和虚基类对比一下,虚继承之后的类在生成对象时,同样会在对象开始保存一个虚指针,只不过虚指针指向的虚表中保存的不是函数的入口地址,而是对象中,基类的属性成员的偏移地址。

 

现在我们已经说明了虚函数用途和使用方式,以及包含多态的意义,需要明确以下几点:

1、虚函数会造成额外的内存开支,所以只应该在类层次结构中并且需要使用多态时才使用

2、虚函数应该是公有的,并且类层次之间的继承也应该是公有的,否则就没什么意义了,

或者有其他的特殊情况,再特殊处理。

0 0
原创粉丝点击