虚调用的定义

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

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

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

  • 虚调用 不能简单的理解成 “对虚函数的调用” ,因为对虚函数的调用很有可能是实调用
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
#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() 是实调用。

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

  • 在构造函数和析构函数中调用虚函数,对虚函数的调用实际上是实调用。这是虚函数被“实调用”的另一个例子。
  • 构造函数中调用虚函数:
    1. 从概念上说,在一个对象的构造函数运行完毕之前,这个对象还没有完全诞生,所以在构造函数中调用虚函数,实际上都是实调用。
    2. 构造函数要先调父类的初始化函数: 因为子类会用到父类资源,比如子类获取父类的变量
    3. 先初始化虚表指针在调用属性初始化和方法体: 因为构造函数会有可能调用虚函数
    4. 先初始化属性在调用方法体: 方法体可能会获取属性
  • 析构函数中调用虚函数:
    1. 析构时,在销毁一个对象时,先调用该对象所属类的析构函数,然后再调用其基类的析构函数。所以,在调用基类的析构函数时,派生类已经被析构了,派生类数据成员已经失效,无法动态的调用派生类的虚函数(在某些情况下会报错,例如纯虚函数)。
    2. 而基类能通过虚表调用虚函数的原因是:基类在自己的析构函数中再次给自己的虚表赋值。在析构对象流程,首先释放子类的所有子类资源,在释放父类所有资源。因为子类资源被释放了,如果调用到父类时虚表没有还原父类的虚表,那么父类析构中有调用虚函数的可能会引起意外的异常。因为指向的函数是一个释放资源的子类函数。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
#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;
}

程序的执行结果是:

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

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

  • 答案是否定的。在实际应用中,绝大多数的虚调用的确是显示借助于指针或者引用来实现,但是可以通过间接的方式来实现虚调用。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
#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() 是一个虚调用,它是在运行时才决定使用派生类中的虚函数还是使用基类中的虚函数。