【问题标题】:What is the most concise yet accurate way to describe what a virtual function is in C++?在 C++ 中描述虚函数是什么的最简洁而准确的方法是什么?
【发布时间】:2011-07-19 05:43:53
【问题描述】:

在评估基本 C++ 知识的面试中,被要求描述什么是虚函数似乎是最常见的问题之一。然而,经过几年的 C++ 编程,我仍然有一种不舒服的感觉,我并不真正了解如何最好地定义它们。

如果我查阅维基百科,我看到虚函数的定义是:

“在面向对象的编程中,虚函数或虚方法是一种函数或方法,其行为可以在继承类中被具有相同签名的函数覆盖”

这个定义看起来简单而优雅,而不是特定于 C++。但对我来说,它似乎没有捕捉到 C++ 中虚函数的概念,因为在继承类中,具有相同签名的函数肯定也可以覆盖非虚函数。

如果我被要求非正式地描述什么是虚函数,我会说一些指针,例如“它是一种方法,当您通过基类指针调用它时,会调用派生类中定义的它的版本相反,如果指针实际上指向派生类的实例”。这似乎不是对这个概念的非常优雅的描述。我知道人们说这就是在 C++ 中实现“多态性”的方式(据我所知,多态性大致是将对象组织成层次结构的整个想法),但我不知道有更好的方式来理解或解释机制而不是通过指针来完成示例。

我想我对虚函数的“指针”描述是否是它们定义的基本内容,还是只是它们在 C++ 中实现的附带内容感到困惑。

【问题讨论】:

  • 这看起来有点主观和争论。

标签: c++ oop function polymorphism virtual


【解决方案1】:

我一直认为这句话抓住了虚函数的精髓:

虚函数是一种定义一系列相关行为的方法,这些行为可由实际必须执行这些行为的实体进行自定义。

如果你忽略虚函数的所有 C++ 主义——拥有虚函数如何使dynamic_cast 能够为类类型的对象工作,它们如何仅在通过指针访问时才被虚拟处理,虚析构函数如何完全与虚拟非析构函数等不同 - 我认为上述陈述是虚拟函数的核心。

我喜欢这句话的主要原因是它以一种将虚函数与编程分离的方式来描述虚函数。您可以通过给出一些具体的类比来使用此定义向非技术人员解释虚函数。例如,“打开灯”的想法可以被认为是一个虚函数,因为当你打开灯时发生的实际机制完全取决于你使用的特定灯(白炽灯?荧光灯?LED? ),但在每种情况下,概念上的想法都是相同的。当然,这不是一个完美的类比,但我认为它很好地表达了这一点。

恕我直言,更笼统地说,如果您曾被要求非正式地描述某事,请尽量与您正在使用的特定编程语言保持距离,如果可能的话,与计算机保持距离。试着想一想这个概念适用的最一般的环境,然后在那个层次上描述它。再说一次,我教的是 CS 入门课程,所以我对这个领域有一点偏见,所以我不知道这在求职面试中的适用性有多大。 :-)

【讨论】:

  • 谢谢!我想这可能是我一直在摸索的。我注意到你没有使用“多态”这个词——你基本上不是在描述这个概念,它在 C++(和其他一些语言)中是通过虚函数机制实现的吗?
  • 我认为这个定义确实触及了多态性的核心思想 - 您可以统一处理具有不同行为的多个对象,同时决定如何对对象做出反应 - 正如您所指出的那样out 是用 C++ 用虚函数实现的。当然,如果你想从高层次上解释某事,扔掉像“多态性”这样的花哨的词可能不会有太大帮助。 :-) 在与非技术人员的对话中,我经常需要检查自己,因为我喜欢使用“端口”、“过载”和“元”等术语来解释事物。
【解决方案2】:

当然,一个非虚函数也可以在继承类中被具有相同签名的函数覆盖。

不,那是不正确的。在这种情况下,该函数只会被重新定义而不是被覆盖。

【讨论】:

  • @snok:您的非正式描述指出了差异——它出现在通过指针或对基类的引用调用时。
  • @snok :从技术上讲,在 C++ 中,只有虚函数会被覆盖。如果在派生类重新定义基类的实现时使用术语 overriding,则 IMO 是不正确的。
  • This C++ 常见问题条目似乎暗示这两个术语可以互换。
【解决方案3】:

您的非正式定义很好地概括了虚拟指针的作用。听起来您还想描述它是如何工作的,但由于 C++ 标准没有指定,任何“如何”描述都特定于特定的实现,而不是一般的 C++ 语言。

C++ 标准是关于行为的,几乎没有关于实现的任何内容。甚至还有“as-if”规则,它允许提供相同可见行为的任何替代实现。

【讨论】:

    【解决方案4】:

    "我想我对虚函数的“指针”描述是否是它们定义的基础,还是只是它们在 C++ 中实现的附带内容感到困惑。"

    这不是偶然的。虚函数的概念仅适用于指针或引用。


    当具有与基类相同签名的派生类方法被覆盖为不同的功能时,需要虚拟函数。

    class Polygon
    {
        public:
        virtual float area()
        {
            std::cout << "\n No formula in general \n" ;
        }
        virtual ~Polygon();
    };
    
    class Square
    {
        public:
        float area()
        {
            std::cout << "\n Side*Side \n" ; 
        }
        ~Square();
    }
    
    Polygon* obj = new Square ;
    obj -> area() ;  // Ok, Square::area() is called. 
    
    Square obj1;
    Polygon& temp = obj1 ; // Ok, Square::area() is called
    
    Square obj2;
    Polygon temp1 = obj2 ; // Polygon::area() is called because of object slicing.
    

    【讨论】:

      【解决方案5】:

      不同之处在于编译器在编译源代码时如何决定将哪个实现“绑定”到方法调用。对于虚函数,选择的实现是基于对象本身的实际具体类型,而不是变量的类型。这意味着当您将对象转换为基类或接口时,将被执行的实现仍然是对象实际是在派生类上定义的实现。

      在 puesdo 代码中

      // for a virtual function
      public class Animal { public virtual void Move() { Print "An animal Moved." } }
      public class Dog: Animal  { public void Move() { Print "A Dog Moved." } }
      
      Animal x = new Dog();
      x.Move()  // this will print "A Dog Moved."
      

      对于非虚函数,选择的实现将基于变量的类型,这意味着当您将对象转换为基类时,(即更改变量的类型)定义的方法实现基类中的内容将由编译器选择并执行,而不是派生类中的实现...

      // for a non-virtual function
      public class Animal { public void Move() { Print "An animal Moved." } }
      public class Dog: Animal  { public void Move() { Print "A Dog Moved." } }
      
      Animal x = new Dog();
      x.Move() // this will print "An animal Moved."
      

      【讨论】:

      • 好吧,我了解虚函数的工作原理。我只是不满意粘贴或描述像上面这样的代码块(归结为“指针定义”)构成了对一般概念的非常好的解释(我认为上面已经简要地给出了)。
      • 一般概念是关于编译器将绑定到哪个实现,上面的代码 sn-p 准确地说明了这一点。定义将是“虚函数是只能绑定到来自与定义函数的类型相同的具体类型的对象的方法调用。”
      【解决方案6】:

      假设 Beta 是 Alpha 的子类,它可以创建一个新方法 area() 也可以添加一个虚拟方法。

      如果您与 Beta 指针交谈,则没有区别。但是,如果您正在与指向 Beta 对象的 Alpha* 交谈,您将获得 Alpha 的方法。除非您将函数声明为虚拟函数。

      Beta 子类有它的函数调度表,它是 Alpha 表的副本,但末尾有额外的 Beta 方法。如果 Beta 仅仅覆盖了一个方法,它将进入调度表的 Beta 部分,因此对 Alpha* 的引用将看不到新方法。但是,如果新方法是虚拟的,它将进入 Beta 类表的 Alpha 部分。

      更重要的是,假设您有一个 Shape 的 Circle 子类。假设您有一个指向 Shape 对象 x 的指针,它恰好是 Circle 的一个实例。 x->area() 只会看到函数表中与 Shape 相关的部分。如果 Circle 做了一个虚拟区域功能,它会出现在表格的 Shape 部分。如果 Circle 只是覆盖 area,那么 area 方法将被放置在表格的 Circle 部分,Shape * x 将看不到新函数。

      在 C++ 中,每个类只有一个函数表。对于习惯于每个对象都有自己的调度表的脚本语言的人来说,这有点令人困惑。脚本语言在这种情况下效率极低。想象一下每个对象占用的所有空间。

      【讨论】:

        【解决方案7】:

        如果面试是关于你在 C++ 方面的知识,我认为引用指针并没有什么罪过。

        您可以简单地说“虚拟函数是一种允许对象表达其类行为的机制,即使它是通过基类指针访问的”。

        不仅如此(如果您考虑“纯虚函数”),它还是一种强制整个类层次结构提供特定方法的机制,而无需在基类中定义默认方法实现。

        【讨论】:

        • 我不确定我是否理解为什么 pure 的概念(用于定义接口)必须与 virtual 的概念(用于允许动态调度)绑定。虽然我可以理解为什么在实践中让它们分开可能没有用。
        • @snok,是的,从技术的角度来看,你有一个观点,但如果你在接受采访,也许你必须展示一些描述在更广泛的范围内提出的概念上下文。不是吗?
        【解决方案8】:

        虚函数是被调用者决定行为的函数。非虚拟函数是调用者决定行为的函数。

        够简洁了吗?

        【讨论】:

        • 所以 foo::doWhatYouLike() 是虚拟的,而 foo::doWhatISay() 是非虚拟的。清如白昼。
        • 好吧,对不起,这听起来很简洁,但我妈妈不明白你的意思。这真的可以作为一个定义吗?这听起来太笼统了,正如我在我的“机智”回应中暗示的那样(抱歉)。
        • @snok 我不确定你妈妈是否有必要的背景来理解什么是虚函数。这就是我对它们的看法,我觉得它抓住了多态性最重要的特征。
        【解决方案9】:

        尽管在 C++ 中使用虚函数需要指针,但我认为指针是附带的想法,并且不依赖于指针的解释更清楚。我认为 templatetypedef 和 Charles_Bretana 达到了主要思想。

        指针似乎潜入虚函数描述的原因是只有指针和引用才能具有不同的运行时和编译时类型。类型为class Foo 的变量在运行时必须包含Foo,因此是否使用虚函数并不重要。但是类型为class Foo *的变量可以指向Foo的任何子类,所以这就是虚函数和非虚函数行为不同的情况。

        【讨论】:

        • 是的,确切地说,如果你在谈论 C++,这就是为什么你必须开始讨论指针以获得可靠的描述的原因。我同意指针不应作为概念定义的基础。
        【解决方案10】:

        理念

        虚拟方法和非虚拟方法的基本区别在于将方法的名称绑定到实际的方法实现。虚拟方法在运行时根据类型绑定。编译时绑定了一个非虚函数。

        稍微以前:

        虚拟方法是一种可以在派生类型中被覆盖的方法。
        这样当虚拟方法被调用(通过指针或引用)时,运行时绑定被应用于选择在最派生版本中定义的方法的版本;基于实际对象的类型(被指向或引用)。

        C++ 笔记

        注意:如果祖先声明的方法与 virtual 具有相同的签名,则即使您不使用 virtual 关键字,该方法也是虚拟的。

        【讨论】:

          【解决方案11】:

          实现动态多态性的编译器辅助机制。 请记住,C++ 还通过泛型编程支持静态多态性。而函数指针一直是实现动态多态的最原始的手段。

          【讨论】:

            猜你喜欢
            • 1970-01-01
            • 1970-01-01
            • 1970-01-01
            • 1970-01-01
            • 2011-04-10
            • 1970-01-01
            • 2017-04-17
            • 2012-06-19
            相关资源
            最近更新 更多