虚调用及其调用的具体形式

1.虚调用的定义

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

2.虚函数的几种实调用的情形

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

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

#include <iostream>
using namespace std;

class A{
public:
    virtual void show(){
        cout<<"in A"<<endl;
    }
};

class B:public A{
public:
    void show(){
        cout<<"in B"<<endl;
    }
};

void func(A a){
    a.show();
}

int main(){
    B b;
    func(b);
}

程序运行输出结果是:in A。在函数func()中,虽然在class A中函数show()被定义为虚函数,但是由于a是类A的一个示例,而不是指向类A对象的指针或者引用,所以函数调用a.show()是实调用,函数的入口地址是在编译阶段静态决定的。

函数调用func(b)的执行过程是这样的:先由对象b通过类A的赋值构造函数,产生一个类A的对象作为函数func()的实参进入函数体。在函数体内,a是一个“纯粹”的类A的对象,与类型B毫无关系,所以a.show()是实调用。

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

在构造函数和析构函数中调用虚函数,对虚函数的调用实际航是实调用。这是虚函数被“实调用”的另一个例子。由于从概念上说,在一个对象的构造函数运行完毕之前,这个对象还没有完全诞生,所以在构造函数中调用虚函数,实际上都是实调用。

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

考察如下例子。

#include <iostream>
using namespace std;

class A{
public:
    virtual void show(){
        cout<<"in A"<<endl;
    }
    A(){show();}
    ~A(){show();}
};

class B:public A{
public:
    void show(){
        cout<<"in B"<<endl;
    }
};

int main(){
    A a;
    B* pb=new B();
    cout<<"after new"<<endl;
    delete pb;
    cout<<"after delete"<<endl;
}

程序的执行结果是: in A in A after new in A after delete in A

在构造类B的对象b时,会先调用基类A的构造函数,如果在构造函数中对show()的调用是虚调用,那么应该打印出“in B”。析构也是如此,对虚函数的调用是实调用。因此,一般情况下,应该避免在构造函数和析构函数中调用虚函数,如果一定要这样做,程序猿必须清楚,这是对虚函数的调用其实是实调用。

3.虚调用的常见形式

设立虚函数的初衷就是想在设计基类的时候,对该基类的派生类实施一定程度的控制。笼统的说,就是“通过基类访问派生类成员”。 因此,虚调用最常见的形式是:通过指向基类的指针或引用来访问派生类对象的虚函数。这种情况较为常见。

不常见形式: 不过由于虚调用是通过查询虚函数表来实现的,而拥有虚函数的对象都可以访问道所属类的虚函数表,所以,一个不常见的做法是通过指向派生类对象的指针或引用调用基类对象的虚函数,考察如下代码。

#include <iostream>
using namespace std;

class A{
public:
    virtual void show(){
        cout<<"in A"<<endl;
    }
};

class B:public A{
public:
    void show(){
        cout<<"in B"<<endl;
    }
};

int main(){
    A a;
    B& br=static_cast<B&>(a);
    br.show();
}

程序输出结果是:in A。通过派生类对象的引用rB实现了对基类中虚函数show()的调用,如果在class A的定义中,将函数show()前面的关键字virtual去掉,那么程序的执行结果就是in B,br.show()就变成了实调用。

4.虚调用一定要借助于指针或引用来实现吗

答案是否定的。在实际应用中,绝大多数的虚调用的确是显示借助于指针或者引用来实现,但是可以通过间接的方式来实现虚调用。

#include <iostream>
using namespace std;

class A{
public:
    virtual void show(){
        cout<<"in A"<<endl;
    }
    void callfunc(){show();}
};

class B:public A{
public:
    void show(){
        cout<<"in B"<<endl;
    }
};

int main(){
    B b;
    b.callfunc();
}

程序的执行结果是:in B。在这个程序中,看不到一个指针或者引用,却发生了虚调用。函数调用b.callfunc()执行的实际上是A::func(),如果在class A中却掉函数show()前面的关键字virtual,那么程序的输出结果是:in A。也就是说,在函数callfunc()中,函数调用show()是一个虚调用,它是在运行时才决定使用派生类中的虚函数还是实用基类中的虚函数。


参考文献

[1]陈刚.C++高级进阶教程[M].武汉:武汉大学出版社,2008[8.6(P296-P299)]

本文参与腾讯云自媒体分享计划,欢迎正在阅读的你也加入,一起分享。

发表于

我来说两句

0 条评论
登录 后参与评论

相关文章

来自专栏marsggbo

C++学习笔记之模板篇

一、模板 不管是函数模板还是类模板,在未初始化前都是不占用内存的。 另外一般来说模板代码不能分开编译,即不能分开写成.h文件和.c文件,需要写成一个文件。 ...

2188
来自专栏软件开发 -- 分享 互助 成长

C++STL之map的基本操作

STL中基本的关联式容器有map和set,它们都是以红黑树作为其底层的结构,具有非常高的查找、删除效率,内容会按照键值自动排序。 使用map的注意事项: 1、关...

2099
来自专栏小白鼠

SPIJava SPIDubbo SPI案例原理

SPI 全称为(Service Provider Interface),是JDK内置的一种服务提供发现机制。一个服务(Service)通常指的是已知的接口或者抽...

1692
来自专栏Hongten

python开发_python中str.format()

2042
来自专栏黑白安全

关于PHP语言在内存中的分配(堆和栈的区别)

本文以PHP语言为例来分析计算机中各段存储区的区别,代码段、堆空间段、代码段、初始化静态常量段。

2992
来自专栏desperate633

我的javascript学习之路_01之js基础2JavaScript对象JavaScript函数JavaScript运算符JavaScript选择语句JavaScript循环语句JavaScript

JavaScript中的对象与java中和其他面向对象语言是基本一致的。如何访问对象,如何访问对象方法,如何新建对象等。都是相当一致的。

994
来自专栏梧雨北辰的开发录

Swift学习:构造器(下)

本篇主要介绍Swift中构造器的一些特殊用法 一、可失败的构造器 顾名思义,这是用于我们构造过程可能失败情况的构造器。失败的原因可能是给构造器传入无效的参数值,...

2777
来自专栏GreenLeaves

Jquery 遍历数组之grep()方法介绍

grep()方法用于数组元素过滤筛选。 grep(array,callback,boolean);方法参数介绍。 array   ---待处理数组 callba...

1965
来自专栏全沾开发(huā)

搞懂JavaScript中的连续赋值

搞懂JavaScript中的连续赋值 前段时间老是被一道题刷屏,一个关于连续赋值的坑。 遂留下一个笔记,以后再碰到有人问这个题,直接...

4016
来自专栏IT可乐

深入理解计算机系统(3.8)------数组分配和访问

  上一篇博客我们讲解了汇编语言中过程(函数)的调用实现。理解数据如何在调用者和被调用者之间传递,以及在被调用者当中局部变量内存的分配以及释放是最重要的。那么这...

21910

扫码关注云+社区

领取腾讯云代金券