c ++ virtual keyword vs overriding function

时间:2017-07-27 17:45:25

标签: c++ polymorphism override virtual

我正在学习c ++并且正在学习虚拟关键字。我已经在互联网上搜索,试图理解它无济于事。我进入我的编辑器并进行了以下实验,期望它打印出基本消息两次(因为我的印象是需要使用virtual关键字来覆盖函数)。但是,它打印出两个不同的消息。有人可以向我解释为什么我们需要虚拟关键字,如果我们可以简单地覆盖函数并且看起来仍然看似多态行为?也许有人可以帮助我和其他人在未来理解虚拟与压倒一切。 (我得到的输出是#34;我是基础"其次是"我是派生的")。

#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;
}

6 个答案:

答案 0 :(得分:12)

考虑以下示例。说明virtualoverride需求的重要方面是c->printMe();。请注意c的类型是Base*,但是由于多态性,它可以正确地从派生类调用重写方法。

#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

答案 1 :(得分:6)

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

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

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

答案 2 :(得分:2)

您之前没有看到此行为,因为您已将b声明为Derived类型,因此编译器知道要使用哪些函数。为了揭示为什么virtual是必要的,你需要混淆:

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

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

    delete b;

    return 0;
}

现在b的类型为Base*,这意味着它将使用Base上的函数以及虚函数表中的任何内容。这会破坏您的实施。您可以通过正确声明virtual来解决问题。

答案 3 :(得分:2)

override是在C ++ 11中添加的新关键字。

你应该使用它,因为:

  • 编译器将检查基类是否包含匹配的virtual方法。这很重要,因为方法名称或其参数列表中的某些拼写错误(允许重载)可能会导致某些内容在实际上没有被覆盖的印象。

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

  • virtual并不代表&#34;覆盖&#34;。在课堂上使用&#34;覆盖&#34;关键字比覆盖一个方法你可以简单地写这个方法省略&#34;虚拟&#34;关键字,覆盖将隐式发生。开发人员在C ++ 11之前编写virtual来表示他们的覆盖意图。简单地说virtual意味着:这个方法可以在子类中重写。

答案 4 :(得分:1)

virtual的意思是,“这实际上不是C函数,即将一系列参数推入堆栈,然后跳转到函数主体的SINGLE不变地址。”

相反,是另一只野兽在运行时在表中查找要执行的函数体的地址。层次结构中的每个类在该表中都有一个条目。函数指针表称为 vtable 。这是用于多态性的RUNTIME机制,该机制会注入额外的代码来执行此查找,然后将 dispatch 分发到功能体的适当专用版本。

此外,当使用此vtable调度机制时,始终通过POINTER访问对象,而不是直接访问(变量或引用)对象。 Foo* foo{makeFoo()}; foo->someMethod()Loo loo{}; loo.someMethod()。因此,使用该技术还需要另一种取消引用权限。

这是整洁的部分:这些指针也可以指向派生类的任何对象,因此,如果您有一个继承自FooChild的类FooParent,则可以使用FoodParent *指向FooParentFooChild

对方法进行调用时,不仅要做普通的C事情,即在堆栈上准备参数,然后跳转到barMethod()的正文,它首先要进行大量的运行时工作列出了barMethod的几种不同实现之一,这些实现针对每个类进行了个性化设置。该表称为vtable。该类层次结构中的每个类在此表中都有一个条目,其中说明该函数体实际上是该特定类的位置,因为它们可以具有不同的类,即使我们使用FooParent *指向其中任何一个的实例,

但这是我们首先要这样做的原因:假设virtual不存在。而您,程序员,则想处理一堆来自类层次结构的对象。好吧,您最终将完成与编译器手动为您注入的代码相同的代码!为了将这些各种类的实例传递到您编写的用于对其进行处理的某些函数中,您需要使用单数大小的类型才能使函数调用代码起作用。因此,请使用指针,因为无论您指向的对象大小如何,指针在您的计算机上(这些天)始终具有相同的大小。好的。所以它是指针。这是使用virtual所必需的类型擦除

然后,您需要一个switch语句或某些分支来分支到它所指向的特定类。但这就是您为编写的每个变体手工编写的代码。真傻很快,您就会意识到,使用指向各种版本的barMethod()进行调用的指针表会更好。这样,您始终可以从每个变体中查找相同的表,而不必重写手写的switch语句等。所以你会这样做。您将实现一个表,其中对于从barMethod()派生的层次结构中的每个类,您都有指向不同FooParent的指针。对于每个类,它们都具有相同的签名(参数列表,返回值等),但机构不同。

您要为每个类别分配一个整数i.d。或类似的东西,并将其用作表的偏移量。也许FooChildAFooChildB是两个不同的类,例如,它们都是从FooParent派生的,所以您可以将A赋给0,将B赋给1,或者类似的东西。然后使用这些偏移量跳入表并获取指针。这就是查找表通常的工作方式。一旦获得了指针,就可以将所有参数推入堆栈,然后跳转到该指针。因此,virtual只是一个关键字,它指示编译器为您将所有这些疯狂的高级代码注入您的代码中,因此您无需手动进行操作。

问题是它是RUNTIME多态性,通常可以通过模板等代替使用COMPILE时间多态性。它给虚拟层次结构中的每个函数调用增加了很多运行时膨胀。对于非热循环,这实际上是很好的。但是对于在系统中始终运行的事物(例如每几毫秒或更长时间)而言,这确实是令人无法接受的膨胀。在绝大多数情况下,您可以在编译时执行所有这些表查找工作,而无需使用元编程,因此运行时非常快。

对于override,令人困惑的混乱本来应该是从一开始就使用的语言,并且应该与virtual关键字处于相同的文本位置。可悲的是,这两个“应该”都没有完成。因此,在过去,您会在类层次结构的最父级中将barMethod()声明为virtual,然后在派生类中将barMethod()声明为virtual。在某些时候,由于怪异的错误,这变得非常烦人。老实说,该功能不是直观的,并且在多年的学习后很难教甚至很难记住。

因此,我们向编译器添加了override和一个提示,以便我们可以捕获错误。它的意思是“不仅此函数是虚拟的,所有疯狂的vtable调度工作都是虚拟的,此外,这是barMethod()的DERIVED重新定义,因此编译器可以检查以确保您匹配参数例如,如果您无意中未能将派生版本的参数列表与父版本完全匹配,而不是覆盖父版本,则编译器只会说:“哦,另一个全新的虚拟成员函数层次结构正在启动,具有不同的参数,这就是根。必须是新的过载集。”

我意识到这是一个超级混乱的说法。但是基本上,如果您有barMethod()barMethod(int)barMethod(int, char*)等等,这些都是不同的函数,彼此之间没有真正的关系。好像每个人都有不同的名字。您可以这样想。本质上,这就是编译器本身通过 name mangling 来考虑的方式。因此,如果随后将它们设置为virtual,则您可能会认为,在层次结构的各个类中进行声明也会将它们也置于单个成员函数虚拟层次结构中。但事实并非如此。如果改为使用override关键字将它们虚拟化,则编译器会注意到barMethod(int) overridebarMethod(int, char*) overrideFooParent中只有barMethod()的任何内容都没有关系。没有参数。但据称它们是压倒一切。错误!那很好。您想要该错误,否则您的代码将发送给客户,并且看起来像在起作用,但绝对没有。

virtual的重点是允许您使用“单指针类型”来表示整个类层次结构的任何实例,但可能会对每个类执行不同的操作。如果程序员没有确保所有派生的重定义也是虚拟的,那将不会发生。并进行覆盖可确保它们不会意外创建新的类层次根。

在现代C ++中,我们认为同时需要virtualoverride太烦人了,并且从视觉上重复grep哪个barMethod()是根版本总是很困难,以及哪些是派生的。因此,他们说:“您可以为衍生的重定义删除virtual关键字,而只需使用override。”这被认为是当今唯一正确的讲话方式。


struct FooParent
{
    // The root has virtual
    virtual void barMethod(); // `= 0;`, potentially as well for "pure virtual," i.e. undefined in the root
    {//some code...}
}

// Original way of doing it. Just use virtual again, but this isn't the root anymore.
struct FooChild_OldSchool : FooParent
{
    virtual void barMethod(); // Total trashmouth.
}

struct FooChild_OverrideDays : FooParent
{
   virtual void barMethod() override; // Naughty mouth.
}

struct FooChild_NonTrashyWay2020 : FooParent
{
  void barMethod() override; // Prim and proper mouth speaking a bizarre langauge.
}

尽管如此,override在语法上却位于参数列表之后的另一个位置,而不是之前。据我所知,这确实是不合逻辑的。我真的希望我们能够解决此问题,并允许override在声明开始时进入virtual所在的位置,或者更好的是,让virtual移至{{1 }}在参数列表之后。到现在为止,它是令人讨厌的不一致和令人困惑的imo。我之所以这么说是因为我相信如果我们不承认它们是疣的话,这些事情将变得难以教each。因为当您学习一种新语言时,您确实需要一个更流利的说话者说:“嘿,这很奇怪而且很谨慎。不要担心。这不是因为您很笨。只是因为我们的语言已经发展并且呆呆的。”

我希望是这样...

override

但不是。也许那是您内部可以想到的方式,然后只需记住在实际键入代码时在语法上添加此怪异的提示即可。想知道关于此的论文是否可以保留5分钟。嗯。

答案 5 :(得分:0)

我认为您的问题是为什么有人在程序中根本使用基类指针来调用派生类。

一种情况是,您希望为程序中的所有派生类提供通用功能。您不想使用不同的派生类类型参数创建相同的函数。 见下文

#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;
}