为什么在此示例中需要后期绑定?

Why is late binding needed in this example?

本文关键字:绑定 为什么      更新时间:2023-10-16

我理解为什么在动态创建子类的对象时需要 virtual 关键字来覆盖,但是在以下示例中,为什么需要后期绑定(虚拟关键字(来覆盖?编译器在编译时无法判断 pa 指向派生类吗?

class A { 
public: int f() { return 'A';}
};
class B : public A {
public: int f() { return 'B';} 
};
int main() {
//I understand this case, since pa is pointing to 
//something created at runtime, virtual keyword is needed
//to return 66 
A* pa;
pa = new B;
cout << pa->f() << endl; //returns 65 
//But why in this case where b2 is created during 
//compile time, compiler doesn't know pa is pointing
//to a derived class? 
B b2; 
pa = &b2; 
cout << pa->f() << endl; //returns 65
}

这个问题实际上并不围绕着编译器是否可以"知道"pa引用的对象的精确类型。它围绕着C++的语义。

当你声明一个方法f时,你是在告诉编译器你希望如何处理对f的调用。在A::f没有声明virtual的情况下,你是说如果调用pa->f()并且pa具有声明的A*类型,您希望编译器在A::f中使用定义。当然,*paA类型的对象,即使它也可能是某种派生类型的A的对象。

另一方面,如果你声明fvirtual,你告诉编译器你希望它引用pa当前指向的对象的最派生类型,并使用来自该类型(或其适当的超类型(的f定义。

C++的语义必须是确定性的。也就是说,作为程序员,您需要能够预测在任何给定情况下将使用哪种f定义。如果你考虑一下,用一种语言编程真的很困难,该规则规定"如果你碰巧能够弄清楚pa指向B类型的对象,请使用B::f,但如果你不确定pa指向什么,请使用A::f"。编译器应该多努力弄清楚pa指向什么?如果将来编译器团队中的某个人想出了如何做出更准确的决定,那么程序的语义是否应该神奇地改变?你对此满意吗?

请注意,实际上,在您提供的两个代码片段中,编译器很有可能可以找出pa指向的对象类型。因此,即使fvirtual,优化编译器也可以省略代码以在vtable中查找正确的方法,而直接调用正确的方法。C++标准中没有任何内容禁止这种优化,我相信这是很常见的。因此,如果将来编译器团队中的某个人找到了确定变量类型的更好方法,它不会改变程序的语义 - 它仍然会调用相同的方法。但这可能会导致程序更快地调用您的方法。这种结果更有可能让你的未来快乐。

这一切都围绕着C++是一种静态类型语言的事实而解决。 因此,编译器将pa视为指向A的指针,而不是指向B的指针(因为您将其声明为这样(,因此,如果您调用pa->f()它将调用A::f()而不是B::f(),除非f()被声明为虚拟。

事实上,这就是虚拟方法的重点 - 当您通过多态指针调用该方法时,调度到正确的方法。

在您展示的非常简单的示例中,是的,编译器可以简单地发现pa是指向动态类型为B的对象的指针。

但是,为了以您描述的方式使用该信息,必须满足以下几点:

  1. 在所有情况下,指针工作的规则都必须需要此信息。
  2. 这些信息必须不仅仅在一个简单的/人为的例子中提供(它不是 - 传递给另一个翻译单元中的函数pa,突然编译器运行处理该单元不知道动态类型是什么;当你之前问这个问题时,给出了一些例子,三个小时前(
  3. 作为程序员,我们通常想要这种行为(我们不需要 - 如果我们愿意,我们可以选择加入virtual,但除此之外,我们想要漂亮的简单行为,其中表达式的类型定义了表达式的含义和操作数的作用(。

最后,请注意,您将动态与自动存储持续时间/分配方法描述为"运行时"与"编译时"并不十分准确,尽管考虑到构建和运行C++程序涉及多少个不同的抽象层,这些术语无论如何都会变得相当毛茸茸。