【问题标题】:c++ virtual keyword vs overriding function时间:2019-01-01 标签:c++virtualkeywordvsoverridingfunction
【发布时间】:2018-01-03 14:08:50
【问题描述】:

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

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

【问题讨论】:

  • 注意:using namespace std; 是一个养成的坏习惯,如果你现在可以停下来,你可能会避免将来遇到很多麻烦。 std:: 前缀的存在是有原因的:它避免与您自己的类、结构和变量发生冲突。
  • 尝试Base *p = new Derived; p-&gt;printMe(); 有和没有virtual
  • 澄清一下——通过指针或对其基类的引用访问对象时,会实现多态行为。
  • 只是一个小小的挑剔:作为一般规则,您应该在问题中包含测试程序的输出
  • @JesperJuhl 进行了建议的更改。谢谢

标签: c++ polymorphism overriding virtual


【解决方案1】:

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

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

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

    delete b;

    return 0;
}

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

【讨论】:

  • 您能解释一下您在第 3 行对 new 关键字的用法吗?我对 new 在这种情况下的工作方式有点生疏。我知道您正在创建一个 base 类型的指针;那么您是否为其分配了足够的内存来容纳派生类?第 3 行是如何工作的?
  • 这只是使用 new 的直接 C++ 分配。它很像 C 的 malloc,但内置了更多智能,而且它会自动调用初始化程序。一本好的 C++ 参考书应该涵盖newdelete 的基础知识。如果您没有,one by the author of C++ 是一个不错的起点。
  • @tadman:正如我对另一个答案的评论,我认为我们最好不要在新人的示例代码中使用裸newdelete,因为它们通常被视为现代 C++ 中的不良做法。引用(或b = &amp;a)也能说明问题。
  • @TristanBrindle 这是一个正确的观点,但不理解new 的作用是理解整个 C++ 的一个大盲点。关于“物体是如何制造的”的讨论最终将不得不进行。
  • @tadman:对象是在堆栈上创建的,或者使用std::make_uniquestd::make_shared(如果它们需要在堆上)。使用原始new/delete 是为了了解背景和遗留代码。
【解决方案2】:

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

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

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

【讨论】:

    【解决方案3】:

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

    【讨论】:

      【解决方案4】:

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

      你应该使用它,因为:

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

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

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

      【讨论】:

      • "virtual 不代表"override"。如果省略它,override 仍然有效。"?我想你的意思是如果你在派生类中省略它,而不是在基类中省略它
      • 是的,这就是我的意思。我改进了文字,使其更加清晰。
      【解决方案5】:

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

      这样一种情况是,当您希望程序中的所有派生类都有一个通用函数时。您不想使用不同的派生类类型参数创建相同的函数。 见下文

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

      【讨论】:

        【解决方案6】:

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

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

        此外,当使用这种 vtable 调度机制时,您总是通过指向对象的指针来访问您的对象,而不是直接访问(变量或引用)它,即。 Foo* foo{makeFoo()}; foo-&gt;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。或类似的东西并将其用作表格的偏移量。例如,也许FooChildAFooChildB 是两个不同的类,它们都派生自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) overridebarMethod(int, char*) overrideFooParent 中的任何内容都没有关系,FooParent 中只有barMethod() 没有参数。但据说他们压倒了一些东西。 ¡ 编译器错误!这很好。你想要那个编译器错误,否则你的代码会交给客户,看起来它正在工作,但绝对不是。

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

        在现代 C++ 中,我们认为同时要求 virtualoverride 太烦人了,而且总是难以直观地 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 分钟。嗯。

        【讨论】:

        • 关于 vtable 调度机制 - “如果派生类是使用指针或对基类的引用来处理的,那么对被覆盖的虚函数的调用将调用派生类中定义的行为。” [@987654321 @
        猜你喜欢
        • 1970-01-01
        • 1970-01-01
        • 2014-11-14
        • 2019-01-09
        • 1970-01-01
        • 1970-01-01
        • 1970-01-01
        • 1970-01-01
        • 1970-01-01
        相关资源
        最近更新 更多