博客
关于我
强烈建议你试试无所不能的chatGPT,快点击我
虚函数的实调用与虚调用
阅读量:4298 次
发布时间:2019-05-27

本文共 5813 字,大约阅读时间需要 19 分钟。

虚调用是相对于实调用而言,它的本质是动态联编。在发生函数调用的时候,如果函数的入口地址是在编译阶段静态确定的,就是是实调用。反之,如果函数的入口地址要在运行时通过查询虚函数表的方式获得,就是虚调用。

虚函数的实调用

不通过指针或者引用调用虚函数

虚调用不能简单的理解成“对虚函数的调用”,因为对虚函数的调用很有可能是实调用。

#include 
using namespace std;class A{public: virtual void show(){ cout<<"A::show()"<
(b).show(); // 2 A a = b; a.show(); // 3 return 0;}

运行结果:

B::show()A::show()A::show()

通过运行结果与反汇编可以看到,以上3种方式在调用虚函数时,函数指针在编译阶段就已经确定,属于实调用。对于第2、3种情况,static_cast<A>(b)a对于编译器来说,都是“纯粹”的类A的实例,与类B毫无关系,所以它们所调用的虚函数的指针在编译阶段就可以确定。

部分反汇编结果:

...20      b.show();   0x0040147e <+30>:    lea    -0x10(%ebp),%eax   0x00401481 <+33>:    mov    %eax,%ecx   0x00401483 <+35>:    call   0x403c14 
21 static_cast
(b).show(); 0x00401488 <+40>: lea -0xc(%ebp),%eax 0x0040148b <+43>: lea -0x10(%ebp),%edx 0x0040148e <+46>: mov %edx,(%esp) 0x00401491 <+49>: mov %eax,%ecx 0x00401493 <+51>: call 0x403bfc
0x00401498 <+56>: sub $0x4,%esp 0x0040149b <+59>: lea -0xc(%ebp),%eax 0x0040149e <+62>: mov %eax,%ecx 0x004014a0 <+64>: call 0x403bc8
...23 a.show(); 0x004014b8 <+88>: lea -0x14(%ebp),%eax 0x004014bb <+91>: mov %eax,%ecx 0x004014bd <+93>: call 0x403bc8

构造函数和析构函数中调用虚函数

在构造函数和析构函数中调用虚函数,对虚函数的调用实际上是实调用。因为从概念上说,在一个对象的构造函数运行完毕之前,这个对象还没有完全诞生,所以在构造函数中调用虚函数,实际上都是实调用。

析构时,在销毁一个对象时,先调用该类所属类的析构函数,然后再调用其基类的析构函数。所以,在调用基类的析构函数时,派生类已经被析构了,派生类数据成员已经失效,无法动态的调用派生类的虚函数。

#include 
using namespace std;class A{public: virtual void show(){ cout<<"A::show()"<
A()"<
~A()"<
B()"<
~B()"<
show(); delete pa;}

运行结果:

===>A()A::show()<===A()===>B()B::show()<===B()B::show()===>~B()B::show()<===~B()===>~A()A::show()<===~A()

从运行结果可以看到,在构造类B的实例时,会先调用基类A的构造函数,如果在构造函数中对show()的调用是虚调用,那么应该打印出B::show(),但运行结果却并不是如此。析构也一样,对虚函数的调用都是实调用。

但我们也应该知道,由于我们将基类的析构函数声明为虚函数,当对pa执行delete操作时,对于析构函数的调用属于虚调用,也就是说,析构函数的指针是从虚函数表中获取的,若我们已经在类B中定义了析构函数,则此时获取的是类B的析构函数指针,这样就使得所有资源都可以成功释放。

部分反汇编结果:

40      pa->show();   0x00401489 <+41>:    mov    0x1c(%esp),%eax   0x0040148d <+45>:    mov    (%eax),%eax   0x0040148f <+47>:    mov    (%eax),%eax   0x00401491 <+49>:    mov    0x1c(%esp),%edx   0x00401495 <+53>:    mov    %edx,%ecx   0x00401497 <+55>:    call   *%eax41      delete pa;   0x00401499 <+57>:    cmpl   $0x0,0x1c(%esp)   0x0040149e <+62>:    je     0x4014b3 
0x004014a0 <+64>: mov 0x1c(%esp),%eax 0x004014a4 <+68>: mov (%eax),%eax 0x004014a6 <+70>: add $0x8,%eax 0x004014a9 <+73>: mov (%eax),%eax 0x004014ab <+75>: mov 0x1c(%esp),%edx 0x004014af <+79>: mov %edx,%ecx 0x004014b1 <+81>: call *%eax

虚函数的虚调用

通过指针或者引用调用虚函数

当通过指针或者引用调用虚函数时,虚函数的指针在编译阶段无法确定,是在运行阶段从虚函数表中的确定位置处获取的。

#include 
using namespace std;class A{public: virtual void show(){ cout<<"A::show()"<
show(); pa->show(); delete pb; delete pa; return 0;}

运行结果:

B::show()B::show()

从下面的反汇编结果可以看到,当通过指针调用虚函数时,其函数指针在编译阶段并没有确定,而是在运行阶段从虚函数表中获取的。当通过指向子类B实例的父类A的指针调用虚函数show()时,由于类的内存空间中保存的是B的虚函数表,且子类B重写了父类A的虚函数show(),此时,虚函数表中父类A的该虚函数指针被子类B的重写虚函数指针所覆盖,所以,通过从虚函数表中获取的是B::show()

部分反汇编结果:

...21      pb->show();   0x004014ad <+77>:    mov    0x1c(%esp),%eax   0x004014b1 <+81>:    mov    (%eax),%eax   0x004014b3 <+83>:    mov    (%eax),%eax   0x004014b5 <+85>:    mov    0x1c(%esp),%edx   0x004014b9 <+89>:    mov    %edx,%ecx   0x004014bb <+91>:    call   *%eax22      pa->show();   0x004014bd <+93>:    mov    0x18(%esp),%eax   0x004014c1 <+97>:    mov    (%eax),%eax   0x004014c3 <+99>:    mov    (%eax),%eax   0x004014c5 <+101>:   mov    0x18(%esp),%edx   0x004014c9 <+105>:   mov    %edx,%ecx   0x004014cb <+107>:   call   *%eax...

“不通过”指针或者引用调用虚函数

在这里加上引号是因为从本质上来说还是通过指针调用的。

#include 
using namespace std;class A{public: virtual void show1(){ cout<<"A::show1()"<
show1(); this->show2(); }};class B:public A{public: void show1(){ cout<<"B::show1()"<
(b).call_show(); return 0;}

运行结果:

0x28ff28B::show1()A::show2()0x28ff2cA::show1()A::show2()

从上面的代码可以看到,在main()中,我们并没有直接调用虚函数,而是通过调用普通成员函数call_show(),并在call_show()中分别调用了虚函数show1()与普通成员函数show2()

结合运行结果可知,在call_show()中,对于show2()的调用属于实调用,在编译阶段就已确定,不管this是指向实例b,还是指向实例b中父类A的拷贝,其函数指针都为A::show2();而对于show1()的调用则明显属于虚调用,其调用的函数根据this指针的不同而不同。

虚调用的不常见形式

由于虚函数指针存放在虚函数表中,我们可以通过存放在实例中的指向虚函数表的指针找到函数指针,这在一定程度上也破坏了类的封装性。当然,在下面代码中如果直接通过函数指针调用虚函数,函数体中this指针的使用会受到限制,但我们可以通过将实例a的地址转为类B的指针或将类B的引用实现对类A私有虚函数的直接调用。

#include 
using namespace std;class A {public: int a;private: virtual void funA1() { cout << "===>A::funA1()" << endl; cout << "this=" << this << endl; cout << "<===A::funA1()" << endl; } virtual void funA2(int a) { cout << "===>A::funA2()" << endl; cout << "this=" << this << endl; this->a = a; cout << "<===A::funA2()" << endl; }};class B {public: virtual void funB1() { cout << "===>B::funB1()" << endl; cout << "this=" << this << endl; cout << "<===B::funB1()" << endl; } virtual void funB2(int a) { cout << "===>B::funB2()" << endl; cout << "this=" << this << endl; cout << "<===B::funB2()" << endl; }};int main() { A a; cout << "a.a=" << a.a << endl; typedef void(*Fun)(); Fun fun = (Fun) *(unsigned int *) *(unsigned int *) &a; fun(); ((B *) &a)->funB2(1); cout << "a.a=" << a.a << endl; ((B &) a).funB2(2); cout << "a.a=" << a.a << endl; return 0;}

运行结果:

a.a=2686868===>A::funA1()this=0x758b4185<===A::funA1()===>A::funA2()this=0x28ff24<===A::funA2()a.a=1===>A::funA2()this=0x28ff24<===A::funA2()a.a=2

在上面的代码中,我们模拟了从虚函数表中获取函数指针并调用该函数的过程,但从运行结果可以看到,this指针并没有指向实例a首地址;而当实例a的地址转为类B的指针或将类B的引用时,获取虚函数指针的虚函数表是类A的。

参考链接

转载地址:http://jhsws.baihongyu.com/

你可能感兴趣的文章
spring boot jpa 实现拦截器
查看>>
jenkins + maven+ gitlab 自动化部署
查看>>
Pull Request流程
查看>>
Lambda 表达式
查看>>
函数式数据处理(一)--流
查看>>
java 流使用
查看>>
java 用流收集数据
查看>>
java并行流
查看>>
CompletableFuture 组合式异步编程
查看>>
mysql查询某一个字段是否包含中文字符
查看>>
Java中equals和==的区别
查看>>
JVM内存管理及GC机制
查看>>
Java:按值传递还是按引用传递详细解说
查看>>
全面理解Java内存模型
查看>>
Java中Synchronized的用法
查看>>
阻塞队列
查看>>
linux的基础知识
查看>>
接口技术原理
查看>>
五大串口的基本原理
查看>>
PCB设计技巧与注意事项
查看>>