c ++虚拟关键字与覆盖函数

c++ virtual keyword vs overriding function

我正在学习 C++,并且正在学习 virtual 关键字。我已经搜索了互联网试图理解它无济于事。我进入我的编辑器并做了以下实验,期望它打印出基本消息两次(因为我的印象是需要 virtual 关键字来覆盖函数)。但是,它打印出两条不同的消息。如果我们可以简单地重写函数并且仍然看似获得多态行为,有人可以向我解释为什么我们需要 virtual 关键字吗?也许将来有人可以帮助我和其他人理解虚拟与覆盖。 (我得到的输出是 "I am the base" 后跟 "I am the derived")。

#include <iostream>

using namespace std;
class Base{
public:
    void printMe(){
        cout << "I am the base" << endl;
    }
};
class Derived: public Base{
public:
    void printMe(){
        cout << "I am the derived" << endl;
    }
};
int main() {
    Base a;
    Derived b;
    a.printMe();
    b.printMe();
    return 0;
}

您在这里看不到行为,因为您已将 b 声明为 Derived 类型,因此编译器知道要使用哪些函数。为了揭示为什么 virtual 是必要的,你需要把事情搞混:

int main() {
    Base a;
    Base *b = new Derived();

    a.printMe();
    b->printMe();

    delete b;

    return 0;
}

现在 bBase* 类型,这意味着它将使用 Base 上的函数加上虚函数 table 中的任何内容。这会破坏您的实施。您可以通过正确声明 virtual.

来修复它

使用您拥有的代码,如果您这样做

Derived derived;
Base* base_ptr = &derived;
base_ptr->printMe();

你认为会发生什么?它不会打印出 I am the derived,因为该方法不是虚拟的,并且分派是根据调用对象的静态类型(即 Base)完成的。如果将其更改为虚拟,则调用的方法将取决于对象的动态类型而不是静态类型。

考虑以下示例。说明需要 virtualoverride 的重要行是 c->printMe();。请注意,c 的类型是 Base*,但是由于多态性,它能够正确地从派生的 class 调用重写的方法。 override 关键字允许编译器强制派生 class 方法与标记为 virtual 的基 class 方法的签名相匹配。如果将 override 关键字添加到派生的 class 函数,则该函数在派生的 class 中也不需要 virtual 关键字,因为隐含了虚拟。

#include <iostream>

class Base{
public:
    virtual void printMe(){
        std::cout << "I am the base" << std::endl;
    }
};

class Derived: public Base{
public:
    void printMe() override {
        std::cout << "I am the derived" << std::endl;
    }
};

int main() {
    Base a;
    Derived b;
    a.printMe();
    b.printMe();
    Base* c = &b;
    c->printMe();
    return 0;
}

输出为

I am the base
I am the derived
I am the derived

override是C++11新增的关键字。

您应该使用它,因为:

  • 编译器将检查基 class 是否包含匹配的 virtual 方法。这一点很重要,因为方法名称或其参数列表中的某些拼写错误(允许重载)可能会让人觉得有些东西被重写了,但实际上并没有。

  • 如果对一个方法使用override,如果不使用override关键字就覆盖了另一个方法,编译器会报错。这有助于在发生符号冲突时检测不需要的覆盖。

  • virtual 并不意味着 "override"。在 class 中不要使用 "override" 关键字而不是覆盖方法,您可以简单地编写此方法省略 "virtual" 关键字,覆盖将隐式发生。开发人员在 C++11 之前编写 virtual 以表明他们的覆盖意图。简单的说virtual的意思是:这个方法可以在一个subclasses.

  • 中重写

我想你的问题是为什么有人会在程序中使用基 class 指针来调用派生 class。

一个这样的例子是当你想在你的程序中为所有派生的 class 提供一个公共函数。您不想使用不同的派生 class 类型参数创建相同的函数。 见下文

#include<iostream>
using namespace std;

class Base{
public:
    virtual void printfunc() { cout<<"this is base class";};
};
class Derived:public Base{
public:
    void printfunc(){cout<<"this is derived class";};
};

void printthis(Base *ptr)
{
    ptr->printfunc();
}

int main()
{
    Derived func;
        printthis(&func);
    return 0;
}

virtual 表示,“这不是真正的 C 函数,即将一系列参数压入堆栈,然后跳转到函数体的单个不变地址。”

相反,是这头野兽在 运行 时间在 table 中查找要执行的函数体的地址。层次结构中的每个 class 在 table 中都有一个条目。函数指针的table被称为vtable。这是 多态性 的运行时机制,它注入额外的代码来执行此查找,然后 dispatch 到函数体的适当专用版本。

此外,当使用此 vtable 调度机制时,您总是通过指向对象的指针访问您的对象,而不是直接访问(变量或引用)它,即。 Foo* foo{makeFoo()}; foo->someMethod() 对比 Loo loo{}; loo.someMethod()。因此,从一开始就需要另一个取消引用才能使用此技术。

这是巧妙的部分:这些指针也可以指向派生的 classes 的任何对象,因此如果您有一个继承自 [=16= 的 class FooChild ],您可以使用 FoodParent * 指向 FooParentFooChild.

当调用该方法时,它不是仅仅执行普通的 C 操作,即在堆栈上准备参数,然后跳转到 barMethod() 的主体,而是执行一堆 运行时间首先查找根据 class 个性化的 barMethod 的几个不同实现之一。那个table叫做vtable。 class 层次结构中的每个 class 在此 table 中都有一个条目,说明函数体真正用于特定 class 的位置,因为它们可以有不同的,即使我们正在使用 FooParent * 指向其中任何一个的实例。

但这就是我们首先要这样做的原因:假设 virtual 不存在。而你,程序员,想要处理一堆来自 class 层次结构的对象。好吧,您最终编写的代码几乎与编译器手动为您注入的代码相同!为了将这些不同 classes 的实例传递到您编写的一些函数中来处理它们,您需要一个单一大小的类型来使函数调用代码起作用。因此,请使用指针,因为指针在您的机器上始终具有相同的大小(如今),无论它们指向的对象大小有多么不同。好的。所以它是指针。这是使用 virtual.

所需的一种 类型擦除

然后你需要一个 switch 语句或其他东西来在它指向的特定 class 上分支。但如果您为编写的每个变体手动编码,那就是这样。太傻了。很快您就会意识到,使用 table 指向您要调用的 barMethod() 各种版本的指针会更好。然后你总是可以从每个变体中查找相同的 table,而不是重写手写的 switch 语句等。所以你会那样做。您将实现一个 table,其中对于从 FooParent 派生的层次结构中的每个 class 都有指向不同 barMethod() 的指针。他们都有相同的签名(参数列表、return 值等),但每个 class.

都有不同的主体

您将为每个 class 分配一个整数 i.d。或类似的东西,并将其用作 table 的偏移量。例如,FooChildAFooChildB 可能是两个不同的 class,它们都派生自 FooParent,因此您可以将 A 分配给 0,将 B 分配给 1,或类似的东西.然后使用这些作为偏移量跳转到 table 并获取指针。这就是查找 table 的一般工作方式。获得指针后,您会将所有参数压入堆栈,然后跳转到该指针。所以 virtual 只是一个关键字,它指示编译器为您将所有这些疯狂的高级代码注入到您的代码中,因此您不必手动执行。

问题是,它是 RUNTIME 多态性,通常可以通过模板等使用 COMPILE 时间多态性。它给虚拟层次结构中的每个函数调用增加了很多 运行时间膨胀。这对于非热循环实际上很好。但是对于 运行 一直在您的系统中(例如每隔几毫秒或更长时间)的事情,这确实是一个无法接受的 table 膨胀量。对于绝大多数情况,您可以在编译时执行与所有 table 查找相同的操作,而不是使用元编程,这样 运行 时间可以非常快。

至于 override,那一团混乱应该从一开始就在语言中,并且应该与 virtual 关键字处于相同的文本位置。可悲的是,这两个“应该”都没有完成。所以在过去,你会在 class 层次结构的最父级中声明 barMethod()virtual,然后还在派生的 class 中声明 barMethod() ] 为 virtual。在某些时候,由于奇怪的错误,这变得非常烦人。老实说,该功能并不直观,并且在了解它多年后很难教授甚至记住。

所以我们添加了 override 以及对编译器的提示,以便我们可以捕获错误。它只是意味着“不仅这个函数是虚拟的,所有那些疯狂的 vtable 调度东西也是如此,而且,这是 barMethod() 的 DERIVED 重新定义,因此编译器可以检查使确保您将参数等与派生它的父 class 完美匹配,因为如果没有此检查,如果您不小心未能将派生版本的参数列表与父版本完全匹配,而不是覆盖父版本,编译器只会说,“哦,另一个全新的虚拟成员函数层次结构正在开始,具有不同的参数,这是根。必须是新的重载集。"

我意识到这是一个非常令人困惑的陈述。但基本上,如果你有 barMethod()barMethod(int)barMethod(int, char*) 等等,这些都是不同的功能,彼此之间没有真正的关系。好像每个人都有不同的名字。你可以在脑海中这样想。它本质上是编译器本身如何看待它,name mangling。因此,如果您随后将它们设为 virtual,您可能会认为在层次结构中的各种 classes 中声明它们也会将它们放入单个成员函数虚拟层次结构中。但事实并非如此。如果您改为使用 override 关键字将它们设为虚拟,编译器会注意到 barMethod(int) overridebarMethod(int, char*) overrideFooParent 中的任何内容都没有关系,后者只有 barMethod()没有参数。但他们应该压倒某些东西。 ¡编译器错误!那很好。您想要那个编译器错误,否则您的代码会发送给客户并且看起来可以正常工作但绝对不是。

virtual 的要点是允许您使用单个指针类型来表示 classes 的整个层次结构的任何实例,但可能对它们中的每一个做不同的事情。如果程序员不确保所有派生的重新定义也是虚拟的,那将不会发生。并且覆盖确保他们不会意外创建新的 class 层次结构根。

在现代 C++ 中,我们认为同时要求 virtualoverride 太烦人了,而且它总是让视觉上更难 grep 哪些 barMethod() 是根版本,以及哪些是派生的。因此他们说,“您可以删除派生重新定义的 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 在句法上位于不同的位置,在参数列表之后,而不是之前。据我所知,这确实不合逻辑。我真的希望我们能解决这个问题,让 overridevirtual 去同一个地方,在声明的开头,或者更好的是,让 virtual 去 [=32] 的地方=] 确实如此,在参数列表之后。就像现在一样,它令人讨厌地不一致和混乱,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 分钟。嗯。