【问题标题】:Overriding vs Virtual覆盖与虚拟
【发布时间】:2011-02-25 08:24:04
【问题描述】:

在函数前面使用保留字 virtual 的目的是什么?如果我希望子类覆盖父函数,我只需声明相同的函数,例如void draw(){}

class Parent { 
public:
    void say() {
        std::cout << "1";
    }
};

class Child : public Parent {
public:
    void say()
    {
        std::cout << "2";
    }
};

int main()
{
    Child* a = new Child();
    a->say();
    return 0;
}

输出为 2。

那么,为什么say() 的标题中需要保留字virtual

非常感谢。

【问题讨论】:

    标签: c++ function virtual overriding


    【解决方案1】:

    如果函数是虚拟的,那么你可以这样做并且仍然得到输出“2”:

    Parent* a = new Child();
    a->say();
    

    这是因为 virtual 函数使用 actual 类型,而非虚拟函数使用 declared 类型。阅读 polymorphism 以更好地讨论您为什么要这样做。

    【讨论】:

    • 你一直这样做,经典的例子是ParentShape,而child 是一种特定类型的形状(如Square)。然后将say 替换为例如draw。你明白为什么这会有用吗?这与 OP 的问题中的示例完全相同,只是用词不同。
    • 好例子! ...但是你为什么一直这样做呢?为什么不 Square* sq = new Square();首先?
    • 你不会一直这样做,你会在适当的时候这样做。如果您正在创建一个绘图应用程序,并让人们选择形状画笔怎么办。您需要一个全局(或至少是对象级)变量,但不知道他们会提前选择什么样的形状。
    【解决方案2】:

    试试看:

    Parent *a = new Child();
    Parent *b = new Parent();
    
    a->say();
    b->say();
    

    没有virtual,都带有打印'1'。添加 virtual,即使它是通过指向 Parent 的指针引用的,孩子也会像 Child 一样工作。

    【讨论】:

    • 所以除非你强制转换对象或使用派生构造函数,否则无法区分重载的常规方法和重载的虚方法?
    【解决方案3】:

    这是我认为多态性如何工作的经典问题。主要思想是你想为每个对象抽象出特定的类型。换句话说:您希望能够在不知道它是孩子的​​情况下调用 Child 实例!

    这是一个例子: 假设您有“Child”类以及“Child2”和“Child3”类,您希望能够通过它们的基类(Parent)引用它们。

    Parent* parents[3];
    parents[0] = new Child();
    parents[1] = new Child2();
    parents[2] = new Child3();
    
    for (int i=0; i<3; ++i)
        parents[i]->say();
    

    您可以想象,这是非常强大的。它使您可以根据需要多次扩展 Parent,并且采用 Parent 指针的函数仍然可以工作。要像其他人提到的那样工作,您需要将该方法声明为虚拟。

    【讨论】:

    • 我认为一个明确的例子会非常感激。
    【解决方案4】:

    如果您不使用virtual 关键字,您不是在覆盖,而是在派生类中定义了一个不相关的方法,它将隐藏基类方法。也就是说,没有virtualBase::sayDerived::say 是不相关的——除了名称巧合。

    当您使用 virtual 关键字(在基类中是必需的,在派生类中是可选的)时,您是在告诉编译器从该基类派生的类将能够覆盖该方法。在这种情况下,Base::sayDerived::say 被视为同一方法的覆盖。

    当您使用基类的引用或指针调用虚方法时,编译器将添加适当的代码,以便调用 final overrider(最派生类中的 override在使用的具体实例的层次结构中定义方法)。请注意,如果您不使用引用/指针而是使用局部变量,编译器可以解析调用,并且不需要使用虚拟调度机制。

    【讨论】:

      【解决方案5】:

      好吧,我自己测试了一下,因为我们可以考虑很多事情:

      #include <iostream>
      using namespace std;
      class A
      {
      public:
          virtual void v() { cout << "A virtual" << endl; }
          void f() { cout << "A plain" << endl; }
      };
      
      class B : public A
      {
      public:
          virtual void v() { cout << "B virtual" << endl; }
          void f() { cout << "B plain" << endl; }
      };
      
      class C : public B
      {
      public:
          virtual void v() { cout << "C virtual" << endl; }
          void f() { cout << "C plain" << endl; }
      };
      
      int main()
      {
          A * a = new C;
          a->f();
          a->v();
      
          ((B*)a)->f();
          ((B*)a)->v();
      }
      

      输出:

      A plain
      C virtual
      B plain
      C virtual
      

      我认为一个好的、简单和简短的答案可能看起来像这样(因为我认为理解得更多的人可以记住的更少,因此需要简短的解释):

      虚拟方法检查指针指向的实例的数据,而经典方法不会因此调用与指定类型对应的方法。

      该功能的要点如下:假设您有一个 A 数组。该数组可以包含 B、C(甚至是派生类型)。如果你想顺序调用所有这些实例的相同方法,你会调用你重载的每一个。

      我觉得这很难理解,显然任何 C++ 课程都应该解释这是如何实现的,因为大多数时候你只是被教导虚函数,你会使用它们,但直到你理解编译器如何理解它们和可执行文件将如何处理调用,你一无所知。

      关于 VFtables 的问题是我从来没有被解释过它添加了什么样的代码,而这显然是 C++ 比 C 需要更多经验的地方,这可能是 C++ 被标记为“慢”的主要原因。早期:事实上,它很强大,但就像所有东西一样,如果你知道如何使用它,它就会很强大,否则你只会“炸掉你的整条腿”。

      【讨论】:

        【解决方案6】:

        当您使用关键字 virtual 时,会创建一个虚函数表来定位实例中的正确方法。然后,即使派生实例被基类指针指向,它仍然会找到方法的正确实现。

        【讨论】:

          【解决方案7】:

          假设我们有如下两个类:-

          class Fruit {
              protected:
              int sweetness;
              char* colour;
              //...
              public:
              void printSweetness() const {
                   cout<<"Sweetness : "<<sweetness<<"\n";
                   return;
              }
          
              void printColour() const {
                   cout<<"Colour : "<<colour<<"\n";
                   return;
              }
          
              virtual void printInfo() const {
                   printSweetness();
                   printColour();
                   return;
              }
          };
          
          class Apple : public Fruit {
                private:
                char* genus;
                //...
                public:
                Apple() {
                    genus = "Malus";
                }
             
                void printInfo() const {
                     Fruit::printInfo();
                     cout<<"Genus : "<<genus<<"\n";
                     return;
                }  
          };
          

          现在假设我们有一些类似下面的函数......

          void f() {
              Fruit* fruitList[100];
              for(int i = 0; i<100 ; i++) {
                  fruitList[i]->printInfo();
              }
              return;
          }
          

          在上述情况下,我们可以调用相同的函数并依赖动态调度机制及其提供的抽象,而无需知道该数组中存储了哪种水果。这大大简化了代码并提高了可读性。并且比使用使代码难看的类型字段要好得多!

          而在重写方法中,我们必须知道我们正在处理什么样的对象,否则会面临可能导致意外结果的对象切片问题。

          注意 -我写这个答案只是为了明确展示好处。

          【讨论】:

            【解决方案8】:

            这是 c++ 编程的一个非常重要的方面——几乎我参加过的每一次面试都会被问到这个问题。

            如果您将 main 更改为:

            int main() { Parent* a = new Child(); a->say(); return 0; }
            

            另外,了解什么是 vtable 是值得的。

            【讨论】:

              猜你喜欢
              • 1970-01-01
              • 1970-01-01
              • 2012-06-19
              • 1970-01-01
              • 1970-01-01
              • 2012-12-02
              • 2012-10-26
              • 2011-05-21
              • 2013-01-15
              相关资源
              最近更新 更多