描述C ++中虚函数的最简洁但最准确的方法是什么?

时间:2011-03-20 18:37:00

标签: c++ oop function polymorphism virtual

被要求描述虚拟功能是什么似乎是评估基本C ++知识的访谈中最常见的问题之一。然而,经过几年的C ++编程,我仍然感到不舒服的感觉,我真的不明白如何最好地定义它们是什么。

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

  

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

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

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

我想我对于虚函数的“指针”描述是否是他们定义的基础,或者只是它们在C ++中实现的附带事件感到困惑。

11 个答案:

答案 0 :(得分:6)

我一直认为这句话抓住了虚拟功能的本质:

  

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

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

我喜欢这种说法的主要原因是它以一种将它们与编程分离的方式描述虚函数。您可以通过给出一些具体的类比,使用此定义向非技术人员解释虚函数。例如,“打开灯”的想法可以被认为是一个虚拟功能,因为当你打开灯时发生的事情的实际机制完全取决于你正在使用的特定光(白炽灯?荧光灯?LED? ),但每种情况下的概念都是一样的。当然,这不是一个完美的比喻,但我认为它足以说明问题。

更一般地说,恕我直言,如果你被要求非正式地描述一些东西,尽可能地尝试远离你正在使用的特定编程语言,如果可能的话,尽可能地远离计算机。尝试考虑概念适用的最一般设置,然后在该级别描述它。然后,我再教授入门CS课程,所以我对这个领域有一点偏见,所以我不知道这在面试工作中有多适用。 : - )

答案 1 :(得分:2)

  

因为具有相同签名的函数肯定也可以在继承类中覆盖非虚函数。

不,这不正确。在这种情况下,该函数仅被重新定义而不被覆盖。

答案 2 :(得分:1)

您的非正式定义是虚拟指针功能的一个很好的总结。听起来你也想描述它是如何工作的,但是由于C ++标准没有规定,任何“how”描述都将特定于特定的实现,而不是一般的C ++语言。

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

答案 3 :(得分:1)

我想我对于虚函数的”指针“描述是否是他们定义的基础,或者是他们在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.

答案 4 :(得分:1)

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

在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."

答案 5 :(得分:0)

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

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

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

更重要的是,假设你有一个Shape的Circle子类。而且假设你有一个指向Shape对象x的指针,它恰好是Circle的一个实例。 x-&gt; area()只能看到函数表中与Shape相关的部分。如果Circle执行虚拟区域功能,它将显示在表格的“形状”部分中。如果Circle仅覆盖区域,则area方法将放置在表格的Circle部分中,而Shape * x将不会看到新函数。

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

答案 6 :(得分:0)

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

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

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

答案 7 :(得分:0)

理念

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

稍微多一点:

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

C ++笔记

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

答案 8 :(得分:0)

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

那简洁吗?

答案 9 :(得分:0)

虽然指针是在C ++中使用虚函数所必需的,但我认为指针是这个想法的偶然因素,并且不依赖于指针的解释更清楚。我认为templatetypedef和Charles_Bretana达成了主旨。

指针似乎渗透到虚函数的描述中的原因是只有指针和引用可以具有不同的运行时与编译时类型。类型为class Foo的变量在运行时必须包含Foo,因此使用虚函数无关紧要。但是类型为class Foo *的变量可以指向Foo的任何子类,因此虚拟函数和非虚函数的行为方式不同。

答案 10 :(得分:0)

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