【问题标题】:Virtual/pure virtual explained虚拟/纯虚拟解释
【发布时间】:2010-11-21 08:42:56
【问题描述】:

如果一个函数被定义为虚函数并且和纯虚函数一样,这究竟意味着什么?

【问题讨论】:

    标签: c++ virtual


    【解决方案1】:

    来自Wikipedia's Virtual function ...

    在面向对象的编程中,在 C++ 和 Object Pascal 等语言中,虚函数或虚方法是一种可继承和可覆盖的函数或方法,可促进动态调度。这个概念是面向对象编程 (OOP) 的(运行时)多态性部分的重要组成部分。简而言之,虚函数定义了要执行的目标函数,但目标函数在编译时可能是未知的。

    与非虚函数不同,当虚函数被重写时,派生最多的版本将用于类层次结构的所有级别,而不仅仅是创建它的级别。因此,如果基类的一个方法调用虚拟方法,则将使用派生类中定义的版本,而不是基类中定义的版本。

    这与非虚函数相反,在派生类中仍然可以被覆盖,但“新”版本只会被派生类及以下使用,但不会改变基类的功能完全没有。

    而..

    纯虚函数或纯虚方法是指如果派生类不是抽象的,则需要由派生类实现的虚函数。

    当纯虚方法存在时,类是“抽象的”,不能单独实例化。相反,必须使用实现纯虚拟方法的派生类。纯虚拟根本没有在基类中定义,所以派生类必须定义它,或者派生类也是抽象的,不能被实例化。只能实例化没有抽象方法的类。

    虚拟提供了一种覆盖基类功能的方法,而纯虚拟需要它。

    【讨论】:

    • 那么...是纯虚拟关键字,还是只是使用的术语?
    • 虚空函数() = 0;是纯虚拟的。 “= 0”表示纯度。
    • 贾斯汀,'纯虚拟'只是一个术语(不是关键字,请参阅下面的答案),用于表示“此功能无法由基类实现。正如 Goz 所说,添加“= 0" 到虚函数的末尾使其成为“纯”
    • 我相信 Stroustrup 说过他想添加一个 pure 关键字,但贝尔实验室即将发布 C++ 的主要版本,而他的经理在后期不允许这样做。添加关键字很重要。
    • 这不是一个好的答案。任何方法都可以被覆盖,而不仅仅是虚拟方法。有关详细信息,请参阅我的答案。
    【解决方案2】:

    我想评论一下维基百科对虚拟的定义,这里有几个人重复了。 [在编写此答案时,] Wikipedia 将虚拟方法定义为可以在子类中覆盖的方法。 [幸运的是,Wikipedia 已经被编辑过,现在它正确地解释了这一点。] 这是不正确的:任何方法,不仅仅是虚拟方法,都可以在子类中被覆盖。 virtual 所做的是为您提供多态性,即能够在运行时选择最衍生的方法覆盖

    考虑以下代码:

    #include <iostream>
    using namespace std;
    
    class Base {
    public:
        void NonVirtual() {
            cout << "Base NonVirtual called.\n";
        }
        virtual void Virtual() {
            cout << "Base Virtual called.\n";
        }
    };
    class Derived : public Base {
    public:
        void NonVirtual() {
            cout << "Derived NonVirtual called.\n";
        }
        void Virtual() {
            cout << "Derived Virtual called.\n";
        }
    };
    
    int main() {
        Base* bBase = new Base();
        Base* bDerived = new Derived();
    
        bBase->NonVirtual();
        bBase->Virtual();
        bDerived->NonVirtual();
        bDerived->Virtual();
    }
    

    这个程序的输出是什么?

    Base NonVirtual called.
    Base Virtual called.
    Base NonVirtual called.
    Derived Virtual called.
    

    Derived 覆盖 Base 的所有方法:不仅是虚拟方法,还包括非虚拟方法。

    我们看到当你有一个指向派生的基指针 (bDerived) 时,调用 NonVirtual 会调用 Base 类的实现。这在编译时解决:编译器看到 bDerived 是 Base*,NonVirtual 不是虚拟的,因此它在 Base 类上进行解析。

    但是,调用 Virtual 会调用 Derived 类的实现。由于关键字 virtual,方法的选择发生在 运行时,而不是编译时。在编译时发生的情况是编译器看到这是一个 Base*,并且它正在调用一个虚方法,因此它插入了对 vtable 的调用而不是类 Base。这个 vtable 在运行时被实例化,因此运行时解析到最派生的覆盖。

    我希望这不会太混乱。简而言之,任何方法都可以被覆盖,但只有虚拟方法才能为您提供多态性,即运行时选择最派生的覆盖。然而,在实践中,重写非虚拟方法被认为是不好的做法并且很少使用,因此很多人(包括撰写该 Wikipedia 文章的人)认为只能重写虚拟方法。

    【讨论】:

    • 仅仅因为 Wikipedia 文章(我无法为它辩护)将虚拟方法定义为“可以在子类中被覆盖的方法”并不排除其他非虚拟方法与可以声明相同的名称。这称为重载。
    • 定义仍然不正确。根据定义,可以在派生类中重写的方法不是虚拟的;该方法是否可以被覆盖与“虚拟”的定义无关。此外,“重载”通常是指在同一个类中具有多个名称和返回类型相同但参数不同的方法;它与“覆盖”非常不同,“覆盖”意味着完全相同的签名,但在派生类中。当它以非多态方式(非虚基)完成时,通常称为“隐藏”。
    • 这应该是公认的答案。那个特定的维基百科文章Which I will take the time to link here since nobody else on this question has done it,完全是垃圾。 +1,好先生。
    • 现在说得通了。谢谢您,好先生,您正确地解释了任何方法都可以被派生类覆盖,变化在于编译器将如何选择在不同情况下调用什么函数。
    • 添加具有相同函数调用的Derived* 可能会有所帮助。否则很好的答案
    【解决方案3】:

    virtual 关键字使 C++ 能够支持多态性。当你有一个指向某个类的对象的指针时,例如:

    class Animal
    {
      public:
        virtual int GetNumberOfLegs() = 0;
    };
    
    class Duck : public Animal
    {
      public:
         int GetNumberOfLegs() { return 2; }
    };
    
    class Horse : public Animal
    {
      public:
         int GetNumberOfLegs() { return 4; }
    };
    
    void SomeFunction(Animal * pAnimal)
    {
      cout << pAnimal->GetNumberOfLegs();
    }
    

    在这个(愚蠢的)示例中,GetNumberOfLegs() 函数根据调用它的对象的类返回适当的数字。

    现在,考虑函数“SomeFunction”。它不关心传递给它的是什么类型的动物对象,只要它是从 Animal 派生的。编译器会自动将任何 Animal 派生类转换为 Animal,因为它是基类。

    如果我们这样做:

    Duck d;
    SomeFunction(&d);
    

    它会输出“2”。如果我们这样做:

    Horse h;
    SomeFunction(&h);
    

    它会输出“4”。我们不能这样做:

    Animal a;
    SomeFunction(&a);
    

    因为 GetNumberOfLegs() 虚函数是纯的,所以无法编译,这意味着它必须通过派生类(子类)来实现。

    纯虚函数多用于定义:

    a) 抽象类

    这些是基类,您必须从它们派生,然后实现纯虚函数。

    b) 接口

    这些是“空”类,其中所有函数都是纯虚函数,因此您必须派生然后实现所有函数。

    【讨论】:

    • 在你的例子中,你不能做#4,因为你没有提供纯虚方法的实现。严格来说不是因为该方法是纯虚拟的。
    • @iheanyi 您不能在基类中为纯虚方法提供实现。因此案例 #4 仍然是错误的。
    【解决方案4】:

    在 C++ 类中,virtual 是指定方法可以被子类覆盖(即由其实现)的关键字。例如:

    class Shape 
    {
      public:
        Shape();
        virtual ~Shape();
    
        std::string getName() // not overridable
        {
          return m_name;
        }
    
        void setName( const std::string& name ) // not overridable
        {
          m_name = name;
        }
    
      protected:
        virtual void initShape() // overridable
        {
          setName("Generic Shape");
        }
    
      private:
        std::string m_name;
    };
    

    在这种情况下,子类可以覆盖 initShape 函数来做一些专门的工作:

    class Square : public Shape
    {
      public: 
        Square();
        virtual ~Square();
    
      protected:
        virtual void initShape() // override the Shape::initShape function
        {
          setName("Square");
        }
    }
    

    纯虚拟这个词是指需要子类实现而基类没有实现的虚函数。通过使用 virtual 关键字并在方法声明的末尾添加 =0 将方法指定为纯虚拟方法。

    因此,如果您想将 Shape::initShape 设为纯虚拟,您可以执行以下操作:

    class Shape 
    {
     ...
        virtual void initShape() = 0; // pure virtual method
     ... 
    };
    

    通过向您的类添加纯虚拟方法,您可以使该类成为abstract base class 这对于将接口与实现分开非常方便。

    【讨论】:

    • 关于“必须由子类实现的虚拟功能”——严格来说并不正确,但如果不是,子类也是抽象的。并且抽象类不能被实例化。此外,“不能由基类实现”似乎具有误导性;我建议“还没有”会更好,因为修改代码以在基类中添加实现没有限制。
    • 而且“getName 函数不能由子类实现”不太对。子类可以实现该方法(具有相同或不同的签名),但该实现不会覆盖该方法。您可以将 Circle 实现为子类并实现“std::string Circle::getName()”——然后您可以为 Circle 实例调用任一方法。但如果通过 Shape 指针或引用使用,编译器将调用 Shape::getName()。
    • 两方面的优点。我试图避免讨论此示例的特殊情况,我将修改答案以使其更加宽容。谢谢!
    • @NickHaddad 旧线程,但想知道为什么您将变量称为m_namem_ 是什么意思?
    • @Tqn 假设 NickHaddad 遵循约定,m_name 是通常称为匈牙利符号的命名约定。 m 表示结构/类的成员,整数。
    【解决方案5】:

    “虚拟”表示该方法可以在子类中被覆盖,但在基类中具有可直接调用的实现。 “纯虚拟”意味着它是一个没有直接可调用实现的虚拟方法。这样的方法必须在继承层次结构中至少被重写一次——如果一个类有任何未实现的虚方法,该类的对象就不能被构造并且编译会失败。

    @quark 指出纯虚方法可以有实现,但由于纯虚方法必须被覆盖,所以不能直接调用默认实现。这是一个带有默认值的纯虚拟方法的示例:

    #include <cstdio>
    
    class A {
    public:
        virtual void Hello() = 0;
    };
    
    void A::Hello() {
        printf("A::Hello\n");
    }
    
    class B : public A {
    public:
        void Hello() {
            printf("B::Hello\n");
            A::Hello();
        }
    };
    
    int main() {
        /* Prints:
               B::Hello
               A::Hello
        */
        B b;
        b.Hello();
        return 0;
    }
    

    根据 cmets,编译是否失败取决于编译器。至少在 GCC 4.3.3 中,它不会编译:

    class A {
    public:
        virtual void Hello() = 0;
    };
    
    int main()
    {
        A a;
        return 0;
    }
    

    输出:

    $ g++ -c virt.cpp 
    virt.cpp: In function ‘int main()’:
    virt.cpp:8: error: cannot declare variable ‘a’ to be of abstract type ‘A’
    virt.cpp:1: note:   because the following virtual functions are pure within ‘A’:
    virt.cpp:3: note:   virtual void A::Hello()
    

    【讨论】:

    • 如果你想实例化一个类的实例,它必须被覆盖。如果您不创建任何实例,那么代码将编译得很好。
    • 编译不会失败。如果没有(纯)虚拟方法的实现,则无法实例化该类/对象。它可能不会链接,但它会编译。
    • @Glen, @tim: 在哪个编译器上?当我尝试编译构建抽象类的程序时,它无法编译。
    • @John 编译只会在您尝试实例化包含 PVF 的类的实例时失败。您当然可以为此类类实例化指针或引用值。
    • 另外,John,下面的说法不太正确:“'Pure virtual' 意味着它是一个没有实现的虚拟方法。”纯虚方法可以有实现。但是您不能直接调用它们:您必须从子类中覆盖并使用基类实现。这允许您提供实现的默认部分。不过,这不是一种常见的技术。
    【解决方案6】:

    virtual 关键字是如何工作的?

    假设 Man 是一个基类, Indian 是从 man 派生的。

    Class Man
    {
     public: 
       virtual void do_work()
       {}
    }
    
    Class Indian : public Man
    {
     public: 
       void do_work()
       {}
    }
    

    将 do_work() 声明为虚拟只是意味着:调用哪个 do_work() 将仅在运行时确定。

    假设我这样做,

    Man *man;
    man = new Indian();
    man->do_work(); // Indian's do work is only called.
    

    如果不使用virtual,则由编译器静态确定或静态绑定,具体取决于调用的对象。因此,如果 Man 的对象调用 do_work(),则即使它指向印度对象,也会调用 Man 的 do_work()

    我认为投票最多的答案具有误导性 - 任何方法,无论是否 virtual 都可以在派生类中具有重写的实现。对于 C++ 的具体引用,正确的区别在于关联函数的运行时(使用 virtual 时)绑定和编译时(未使用 virtual 但方法被覆盖并且基指针指向派生对象时)绑定。

    似乎还有另一个误导性的评论说,

    “贾斯汀,'纯虚拟'只是一个术语(不是关键字,请参阅我的回答 下面) 用来表示“这个功能不能由基础实现 类。”

    这是错误的! 纯虚函数也可以有实体并且可以实现!事实上,抽象类的纯虚函数可以被静态调用!两位非常优秀的作者是 Bjarne Stroustrup 和 Stan Lippman.... 因为他们编写了语言。

    【讨论】:

    • 不幸的是,一旦一个答案开始获得投票,所有其他答案都将被忽略。即使他们可以做得更好。
    【解决方案7】:

    虚函数是在基类中声明并由派生类重新定义的成员函数。 虚函数是按继承顺序分层的。 当派生类不覆盖虚函数时,将使用其基类中定义的函数。

    纯虚函数是不包含与基类相关的定义的函数。 它在基类中没有实现。任何派生类都必须重写此函数。

    【讨论】:

      【解决方案8】:

      默认情况下使用静态方法绑定的 Simula、C++ 和 C#,程序员可以通过将它们标记为虚拟来指定特定方法应使用动态绑定。 动态方法绑定是面向对象编程的核心。

      面向对象编程需要三个基本概念:封装、继承和动态方法绑定。

      封装允许实现细节 隐藏在 a 后面的抽象 简单的界面。

      继承允许将新的抽象定义为 一些扩展或改进 现有的抽象,获得一些 或其所有特征 自动地。

      动态方法绑定允许新抽象显示其新的 即使在上下文中使用时的行为 这需要旧的抽象。

      【讨论】:

        【解决方案9】:

        虚拟方法可以被派生类覆盖,但需要在基类中实现(将被覆盖的那个)

        纯虚方法没有实现基类。它们需要由派生类定义。 (所以从技术上讲,覆盖不是正确的术语,因为没有什么可以覆盖的)。

        当派生类覆盖基类的方法时,虚拟对应于默认的 java 行为。

        纯虚方法对应于抽象类中抽象方法的行为。而一个只包含纯虚方法和常量的类将是接口的 cpp-pendant。

        【讨论】:

          【解决方案10】:

          纯虚函数

          试试这个代码

          #include <iostream>
          using namespace std;
          class aClassWithPureVirtualFunction
          {
          
          public:
          
              virtual void sayHellow()=0;
          
          };
          
          class anotherClass:aClassWithPureVirtualFunction
          {
          
          public:
          
              void sayHellow()
              {
          
                  cout<<"hellow World";
              }
          
          };
          int main()
          {
              //aClassWithPureVirtualFunction virtualObject;
              /*
               This not possible to create object of a class that contain pure virtual function
              */
              anotherClass object;
              object.sayHellow();
          }
          

          在类 anotherClass 中删除函数 sayHellow 并运行代码。你会得到错误!因为当一个类包含一个纯虚函数时,不能从该类创建对象并且它被继承,那么它的派生类必须实现该函数。

          虚拟函数

          尝试其他代码

          #include <iostream>
          using namespace std;
          class aClassWithPureVirtualFunction
          {
          
          public:
          
              virtual void sayHellow()
              {
                  cout<<"from base\n";
              }
          
          };
          
          class anotherClass:public aClassWithPureVirtualFunction
          {
          
          public:
          
              void sayHellow()
              {
          
                  cout<<"from derived \n";
              }
          
          };
          int main()
          {
              aClassWithPureVirtualFunction *baseObject=new aClassWithPureVirtualFunction;
              baseObject->sayHellow();///call base one
          
              baseObject=new anotherClass;
              baseObject->sayHellow();////call the derived one!
          
          }
          

          这里 sayHellow 函数在基类中被标记为虚拟。它表示编译器尝试在派生类中搜索该函数并实现该函数。如果找不到,则执行基类。谢谢

          【讨论】:

          • 哈哈,我花了 30 秒的时间才明白这里出了什么问题...... HelloW :)
          【解决方案11】:

          “虚函数或虚方法是一种函数或方法,其行为可以在继承类中被具有相同签名的函数覆盖” - 维基百科

          这不是对虚函数的一个很好的解释。因为,即使成员不是虚拟的,继承类也可以覆盖它。你可以自己试试看。

          当函数将基类作为参数时,差异就显现出来了。当您将继承类作为输入时,该函数使用覆盖函数的基类实现。但是,如果该函数是虚函数,它会使用派生类中实现的函数。

          【讨论】:

            【解决方案12】:
            • 虚拟函数必须在基类和派生类中都有定义,但不是必需的,例如 ToString() 或 toString() 函数是一个虚拟函数,因此您可以通过在用户中覆盖它来提供自己的实现 -定义类。

            • 虚拟函数在普通类中声明和定义。

            • 纯虚函数必须声明以“= 0”结尾,并且只能在抽象类中声明。

            • 具有纯虚函数的抽象类不能定义纯虚函数,因此这意味着必须在派生自该抽象类的类中提供实现。

            【讨论】:

            • 与@rashedcs 相同的注释:确实,纯虚函数可以有它的定义......
            猜你喜欢
            • 1970-01-01
            • 1970-01-01
            • 1970-01-01
            • 1970-01-01
            • 2019-02-21
            • 2014-05-25
            • 1970-01-01
            相关资源
            最近更新 更多