虚函数的实调用与虚调用

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

虚函数的实调用

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

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

#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的。

参考链接

虚调用_百度百科

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值