为什么在运行时处理虚拟函数

Why are virtual functions handled at runtime?

本文关键字:虚拟 函数 处理 运行时 为什么      更新时间:2023-10-16

编译器当然足够聪明,可以在某些情况下准确地推断出您想要的函数,但为什么其他情况需要运行时支持?

因为我们并不总是知道在运行时将面对什么实例。

例如,您有类:SuperClassSubclass1Subclass2,它们都有一个方法doACoolThing()。用户按下按钮012,然后根据他的输入,创建适当类的实例,并调用其doACoolThing()方法。

我们(以及编译器)无法确定在运行时将调用什么类的方法。

这就是为什么这样的技巧需要运行时支持。

一个小例子来说明一个想法(附言:不要这样写代码,这里只是为了说明多态性:):

#include <iostream>
using namespace std;
class SuperClass
{
public:
virtual void doACoolThing();
};
void SuperClass::doACoolThing()
{
cout << "Hello from the SuperClass!" << endl;
}
class Subclass1 : public SuperClass
{
virtual void doACoolThing() override;
};
void Subclass1::doACoolThing()
{
cout << "Hello from the Subclass1" << endl;
}
class Subclass2 : public SuperClass
{
virtual void doACoolThing() override;
};
void Subclass2::doACoolThing()
{
cout << "Hello from the Subclass2" << endl;
}
int main()
{
int userInput;
cout << "Enter 0, 1 or 2: ";
cin >> userInput;
SuperClass *instance = nullptr;
switch (userInput)
{
case 0: 
instance = new SuperClass();
break;
case 1:
instance = new Subclass1();
break;
case 2:
instance = new Subclass2();
break;
default:
cout << "Unknown input!";
}
if (instance)
{
instance->doACoolThing();
delete instance;
}
return 0;
}

考虑以下代码:

Derived1 var1 = <something>;
Derived2 var2 = <something>;
int x;
cin >> x;
Base *baseptr = x ? &var1 : &var2;
baseptr->virtfun();

编译器不知道用户将输入什么,因此无法判断baseptr是指向Derived1还是指向Derived2的实例。

假设您依赖用户输入来决定要创建哪个子类。

class Base
{
public:
void f();
}
class Derived1: public Base
{
public:
void f();
}
class Derived2: public Base
{
public:
void f();
}
int choice;
cin >> choice;
Base *pB = NULL;
if (choice == 1)
{
pB = new Derived1;
}
else
{
pB = new Derived2;
}
pB->f();

如果没有虚拟函数,如果您想根据不同的实例选择f,编译器如何知道在运行时调用f的正确版本?只是没有办法。

原因包括:

  • 只有在运行时才知道一些输入(键盘、鼠标、文件、数据库、网络、硬件设备等),这些输入将决定实际的数据类型,从而决定需要调用的成员函数

  • 没有内联的函数以及从具有不同派生对象的许多地方调用的函数将需要进行虚拟调度——静态调度的替代方案相当于每种类型的"实例化"(ala模板,并可能隐含一些代码膨胀)

  • 使用虚拟调度的函数可以在链接到可执行文件的对象中编译,并使用指针调用它们在编译时从未知道的参数类型

  • 虚拟调度可以提供一种"编译防火墙"(很像指向Implementation或pImpl习语的指针),这样,对定义函数实现的文件的更改就不需要对包含接口的头进行更改,这意味着客户端代码需要重新链接,而不需要重新编译:这可以在企业环境中节省大量时间

  • 在程序的各个阶段跟踪类型通常太复杂了:即使可能从代码的编译时分析中知道,编译器也只需要付出有限的努力来识别编译时常数/确定性数据,这并不涉及跟踪任意复杂的操作,比如说,当调用最终被虚拟调度时,可能会使用的指向基的指针的常量。

    • 容器和实现状态机的变量尤其如此,它们具有关于何时应删除指向基的指针并重置为不同派生类型的复杂规则