virtual 的意思是,“这不是一个真正的 C 函数,即将一系列参数压入堆栈,然后跳转到函数体的单个不变地址。”
相反,它是另一种野兽,它在运行时在表中查找要执行的函数体的地址。层次结构中的每个类在该表中都有一个条目。函数指针表称为vtable。这是 polymorphism 的一种 RUNTIME 机制,它注入额外的代码来执行此查找,然后 dispatch 到函数体的适当专用版本。
此外,当使用这种 vtable 调度机制时,您总是通过指向对象的指针来访问您的对象,而不是直接访问(变量或引用)它,即。 Foo* foo{makeFoo()}; foo->someMethod() 与 Loo loo{}; loo.someMethod()。因此,使用此技术需要从一开始就取消引用。
这是简洁的部分:这些指针也可以指向派生类的任何对象,所以如果你有一个继承自 FooParent 的类 FooChild,你可以使用 FoodParent * 指向 @987654328 @ 或 FooChild。
当对方法进行调用时,它不是像普通的 C 那样在堆栈上准备参数,然后跳转到 barMethod() 的主体,而是先做一堆运行时工作来查找一个barMethod 的几个不同实现,每个类都是个性化的。该表称为 vtable。类层次结构中的每个类在此表中都有一个条目,说明函数体真正用于该特定类的位置,因为它们可以有不同的,即使我们使用FooParent * 指向它们中的任何一个实例。
但这就是我们首先要这样做的原因:假设virtual 不存在。而你,程序员,想要处理来自类层次结构的一堆对象。好吧,您最终会编写与编译器手动为您注入的代码相同的东西!为了将这些不同类的实例传递到您编写的用于处理它们的某个函数中,您需要一个大小奇特的类型才能使函数调用代码正常工作。所以,使用指针,因为指针在你的机器上总是相同的大小(现在),不管它们指向的对象的大小有多么不同。好的。所以指针它是。这是使用virtual 所必需的一种类型擦除。
然后你需要一个switch 语句或其他东西来分支它原来指向的特定类。但是,如果您为您编写的每个变体手动编码,那就是这样。这很愚蠢。很快您就会意识到最好使用指向您的各种版本的barMethod() 的指针表来调用。然后你总是可以从每个变体中查找同一个表,而不是重写手工编码的 switch 语句等。所以你会这样做。您将实现一个表,其中为从FooParent 派生的层次结构中的每个类指向不同的barMethod()s。对于每个类,它们都具有相同的签名(参数列表、返回值等),但具有不同的主体。
您将为每个类分配一个整数 i.d。或类似的东西并将其用作表格的偏移量。例如,也许FooChildA 和FooChildB 是两个不同的类,它们都派生自FooParent,因此您可以将A 分配给0,将B 分配给1,或类似的东西。然后将它们用作偏移量以跳入表格并获取指针。这就是查找表的一般工作方式。获得指针后,您会将所有参数压入堆栈,然后跳转到该指针。所以virtual 只是一个关键字,它指示编译器为你将所有这些疯狂的高级代码注入你的代码中,这样你就不必手动去做了。
问题是,它是 RUNTIME 多态性,而通常可以通过模板等方式使用编译时多态性。它为虚拟层次结构中的每个函数调用增加了很多运行时膨胀。对于非热循环来说,这实际上很好。但是对于在你的系统中一直运行的东西(比如每隔几毫秒或更长时间),这确实是一个不可接受的膨胀量。在绝大多数情况下,您可以在编译时执行与所有表查找相同的操作,而不是使用元编程,这样运行时就可以非常快。
至于override,混乱的混乱从一开始就应该在语言中,并且应该与virtual 关键字处于相同的文本位置。可悲的是,这两个“应该”都没有完成。所以在过去,你会在类层次结构的最父级中将barMethod() 声明为virtual,然后在派生类中将barMethod() 声明为virtual。在某些时候,由于奇怪的错误,这变得非常烦人。老实说,该功能并不直观,并且在了解它多年后很难教授甚至记住。
所以我们向编译器添加了override 以及提示,以便我们可以捕获错误。它只是意味着“不仅这个函数是虚拟的,所以做所有疯狂的 vtable 调度的东西,而且,这是一个 DERIVED 重新定义 barMethod(),所以编译器可以检查以确保你完美匹配参数等与派生它的父类,因为如果没有这个检查,如果你不小心没有将派生版本的参数列表与父版本完全匹配,而不是覆盖父版本,编译器只会说,“哦,另一个完全新的虚拟成员函数层次结构正在启动,具有不同的参数,这是根。必须是新的重载集。”
我意识到这是一个非常令人困惑的陈述。但基本上,如果你有barMethod() 和barMethod(int) 和barMethod(int, char*) 等等,这些都是不同的功能,彼此之间没有真正的关系。好像每个人都有不同的名字。你可以在脑海中这样想。它本质上是编译器本身的想法,name mangling。因此,如果您随后将它们设为virtual,您可能会认为在层次结构中的各种类中声明它们也会将它们放入单个成员函数虚拟层次结构中。但事实并非如此。如果您使用override 关键字将它们变为虚拟,编译器会注意到barMethod(int) override 和barMethod(int, char*) override 与FooParent 中的任何内容都没有关系,FooParent 中只有barMethod() 没有参数。但据说他们压倒了一些东西。 ¡ 编译器错误!这很好。你想要那个编译器错误,否则你的代码会交给客户,看起来它正在工作,但绝对不是。
virtual 的重点是允许您使用单指针类型来表示整个类层次结构的任何实例,但可能对每个实例执行不同的操作。如果程序员没有确保所有派生的重新定义也是虚拟的,那将不会发生。并且覆盖确保它们不会意外创建新的类层次结构根。
在现代 C++ 中,我们认为同时要求 virtual 和 override 太烦人了,而且总是难以直观地 grep 哪个barMethod()s 是根版本,哪些是派生的.所以他们说,“你可以删除派生重定义的virtual关键字,只使用override。”这被认为是当今唯一正确的说话方式。
struct FooParent
{
// The root has virtual
virtual void barMethod(){ /* body */ } // or `=0` for "pure virtual"
}
// Original way of doing it. Just use virtual again, but this isn't the root now. This is a derived class.
struct FooChild_OldSchool : FooParent
{
virtual void barMethod(); // Total trashmouth. Bug prone.
}
struct FooChild_OverrideDays : FooParent
{
virtual void barMethod() override; // Naughty mouth. Using both.
}
struct FooChild_NonTrashyWay2020 : FooParent
{
void barMethod() override; // Prim and proper mouth. Using only override in the derived class.
}
奇怪的是,override 在语法上位于不同的位置,位于参数列表之后,而不是之前。据我所知,这真的不合逻辑。我真的希望我们能解决这个问题,并允许override 在声明的开头与virtual 相同,或者更好的是,让virtual 在参数列表之后与override 相同.就像现在一样,它令人讨厌的不一致和令人困惑,imo。我这么说是因为我相信如果我们不承认它们是疣,这些东西就会变得不可教。因为当你学习一门新语言的时候,你真的需要一个更流利的说话者来说,“嘿,这很奇怪和有毛病。别担心。这不是因为你很笨。只是因为我们的语言已经进化了不靠谱。”
我希望是这样的......
struct FooChild_HowIWishItWas : FooParent
{
override void barMethod();
}
// OR EVEN BETTER! Allow us to change the location of virtual!
struct FooParent_HowIWishItWasEvenMore
{
void barMethod() virtual;
}
但事实并非如此。不过,这也许就是您在内部可以想到的方式,然后记住在实际键入代码时在语法上添加这种奇怪的怪异。想知道这方面的论文是否能存活 5 分钟。嗯。