虚调用是相对于实调用而言,它的本质是动态联编。在发生函数调用的时候,如果函数的入口地址是在编译阶段静态确定的,就是是实调用。反之,如果函数的入口地址要在运行时通过查询虚函数表的方式获得,就是虚调用。
虚函数的实调用
不通过指针或者引用调用虚函数
虚调用不能简单的理解成“对虚函数的调用”,因为对虚函数的调用很有可能是实调用。
#include <iostream>
using namespace std;
class A{
public:
virtual void show(){
cout<<"A::show()"<<endl;
}
};
class B:public A{
public:
void show(){
cout<<"B::show()"<<endl;
}
};
int main(){
B b;
b.show(); // 1
static_cast<A>(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 <B::show()>
21 static_cast<A>(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 <A::A(A const&)>
0x00401498 <+56>: sub $0x4,%esp
0x0040149b <+59>: lea -0xc(%ebp),%eax
0x0040149e <+62>: mov %eax,%ecx
0x004014a0 <+64>: call 0x403bc8 <A::show()>
...
23 a.show();
0x004014b8 <+88>: lea -0x14(%ebp),%eax
0x004014bb <+91>: mov %eax,%ecx
0x004014bd <+93>: call 0x403bc8 <A::show()>
构造函数和析构函数中调用虚函数
在构造函数和析构函数中调用虚函数,对虚函数的调用实际上是实调用。因为从概念上说,在一个对象的构造函数运行完毕之前,这个对象还没有完全诞生,所以在构造函数中调用虚函数,实际上都是实调用。
析构时,在销毁一个对象时,先调用该类所属类的析构函数,然后再调用其基类的析构函数。所以,在调用基类的析构函数时,派生类已经被析构了,派生类数据成员已经失效,无法动态的调用派生类的虚函数。
#include <iostream>
using namespace std;
class A{
public:
virtual void show(){
cout<<"A::show()"<<endl;
}
A(){
cout<<"===>A()"<<endl;
show();
cout<<"<===A()"<<endl;
}
virtual ~A(){
cout<<"===>~A()"<<endl;
show();
cout<<"<===~A()"<<endl;
}
};
class B:public A{
public:
void show(){
cout<<"B::show()"<<endl;
}
B(){
cout<<"===>B()"<<endl;
show();
cout<<"<===B()"<<endl;
}
~B(){
cout<<"===>~B()"<<endl;
show();
cout<<"<===~B()"<<endl;
}
};
int main(){
A* pa=new B();
pa->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 *%eax
41 delete pa;
0x00401499 <+57>: cmpl $0x0,0x1c(%esp)
0x0040149e <+62>: je 0x4014b3 <main()+83>
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 <iostream>
using namespace std;
class A{
public:
virtual void show(){
cout<<"A::show()"<<endl;
}
};
class B:public A{
public:
virtual void show(){
cout<<"B::show()"<<endl;
}
};
int main(){
B *pb = new B();
A *pa = new B();
pb->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 *%eax
22 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 <iostream>
using namespace std;
class A{
public:
virtual void show1(){
cout<<"A::show1()"<<endl;
}
void show2(){
cout<<"A::show2()"<<endl;
}
void call_show(){
cout<<this<<endl;
this->show1();
this->show2();
}
};
class B:public A{
public:
void show1(){
cout<<"B::show1()"<<endl;
}
void show2(){
cout<<"B::show2()"<<endl;
}
};
int main(){
B b;
b.call_show();
static_cast<A>(b).call_show();
return 0;
}
运行结果:
0x28ff28
B::show1()
A::show2()
0x28ff2c
A::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 <iostream>
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
的。
参考链接