虚拟成员函数的目的是什么?

12
在C++中,函数重写和虚函数有什么区别?
虚成员函数可以在派生类中被重写。 在派生类中重新定义一个函数被称为函数重写。
为什么我们需要虚函数呢?

6
你是从哪本书学习C++的? - anon
7
您正在提出一个问题,听起来像是“汽车和三明治有什么区别?”这种问题没有任何有意义的“区别”。它们之间没有任何关系,不足以为它们之间的“区别”提出问题。要回答您的问题,首先需要理解这样一个奇怪的问题如何产生。是否可以详细说明一下? - AnT stands with Russia
7
@andrey,@neil,你们为什么这么敌对? - erick2red
7
@erick2red:敌对?我一点也不敌对。我说的话完全中立且直截了当。我理解有些人可能更喜欢在交谈中得到更多的赞美和甜言蜜语,但同时我认为在技术论坛中这并非必须。 - AnT stands with Russia
1
此外,当我说“首先需要了解这样一个奇怪的问题是如何产生的”时,这不仅仅是一个修辞性的话语或隐晦的侮辱。我确实想知道这个问题是怎么出现的,因为这确实是到达OP困惑根源的最佳(或者唯一)方式。 - AnT stands with Russia
显示剩余9条评论
10个回答

12

虚函数/方法简单来说就是一个函数,其行为可以在子类(或者在C++中称作派生类)中被重写,通过重新定义函数如何工作(使用相同的签名)。

想象一个哺乳动物基类有一个speak函数。这个函数是void类型,并且只是显示一个哺乳动物的说话方式。当你从这个类继承时,你可以重写speak方法,使得狗发出“汪汪!”的声音,而猫则发出“喵喵”的声音。

你的问题似乎问的是有什么区别,实际上没有区别,因为虚函数可以重写这些函数的行为。你可能想了解重载函数和覆盖函数之间的区别。

重载函数是指创建一个具有相同名称但不同参数的函数,即不同数量和类型的参数。这里是关于C++中重载的解释,IBM网站上有说明。

重载(仅适用于C ++)如果在同一范围内为函数名称或运算符指定多个定义,则已重载该函数名称或运算符。分别在重载函数(仅限C ++)和重载运算符(仅限C ++)中描述了重载的函数和运算符。

重载声明是使用与先前在同一范围内声明的声明相同的名称声明的声明,但两个声明具有不同的类型。

如果调用重载的函数名称或运算符,则编译器通过比较您用于调用函数或运算符的参数类型与定义中指定的参数类型来确定要使用的最合适的定义。选择最合适的重载函数或运算符的过程称为重载决议,如重载决议(仅限C ++)中所述。

至于需要虚函数的情况的完整理性原因,这篇博客文章给出了一个很好的解释:http://nrecursions.blogspot.in/2015/06/so-why-do-we-need-virtual-functions.html


在C++术语中,“派生类”。 - anon
2
一个小问题:你说重载“意味着创建一个具有相同名称但不同参数的函数”。我只是想删除该语句中的“返回类型”部分……在当前的C++标准中,它们不被认为是重载决议的一部分。 - Mike Ellery

9

在多态性方面,函数重载和 virtual 函数的区别变得非常重要。特别是当使用基类的引用或指针时。

基本设置

在 C++ 中,任何派生类都可以传递给需要基类对象的函数。(另请参见 SlicingLSP)。给定:

struct Base_Virtual
{
  virtual void some_virtual_function();
};

struct Base_Nonvirtual
{
  void some_function();
};

void Function_A(Base_Virtual * p_virtual_base);
void Function_B(Base_Nonvirtual * p_non_virtual_base);

在上面的代码中,有两个基类,一个声明了一个虚方法,另一个声明了一个非虚函数。
声明了两个需要指向各自基类的指针的函数。

派生类

现在让我们测试多态性,特别是虚拟和非虚拟(覆盖方法)。这些结构:
struct Derived_From_Virtual
: public Base_Virtual
{
  void some_virtual_function(); // overrides Base_Virtual::some_virtual_function()
};

结构体Derived_From_Nonvirtual: public Base_Nonvirtual { void some_function(); }

根据C++语言规定,我可以将指向Derived_From_Virtual的指针传递给Function_A,因为Derived_From_Virtual继承自Base_Virtual。我也可以将指向Derived_From_Nonvirtual的指针传递给Function_B。

virtual和覆盖之间的区别

在Base_Virtual中使用virtual修饰符告诉编译器Function_A将使用Derived_From_Virtual::some_virtual_function()而不是Base_Virtual中的方法。这是因为该方法是虚拟的,最终定义可能位于未来或派生类中。实际定义表示使用包含定义的最终派生类中的方法。

当将指向“Derived_From_Nonvirtual”指针传递给“Function_B”时,编译器将指示函数使用基类“Base_Nonvirtual::some_function()”的方法。派生类中的“some_function()”方法是与基类无关的独立方法。
虚拟和重写之间的主要区别在于多态性。

8

请查看C++ FAQ lite,http://www.parashift.com/c++-faq-lite/。它可能是初学者最好的C++资源之一。它详细介绍了虚函数和覆盖。

我个人发现C++ FAQ是我学习C++的优秀资源。其他人有不同的意见,因人而异。


4
C++ FAQ不是入门级的教材,而是一本参考资料。 - Hassan Syed
如果你还不懂C++,一份告诉你你的C++有多烂的FAQ也帮不了你! - John Dibling
3
也许你正在混淆 C++FAQ lite 和 C++ FQA,前者是 comp.lang.c++ 中经常被提出的问题的汇编,而后者只是基于可疑事实对这种语言的抱怨。 - David Rodríguez - dribeas
1
@dribeas - David Rodrigues:我认为你对C++的偏见影响了你对Yossi Kreinin的文章的看法。我自己也是C++的粉丝,但我发现http://yosefk.com/比parashift.com FAQ更有信息量。 - just somebody
1
@ 就是有人。我在“fqa”中没有找到任何有用的东西。你在那里找到了什么有启发性的信息? - Nemanja Trifunovic
链接坏了? - Smar

4
这更像是对这个答案的跟进,而不是一个独立的答案。 virtual是一个请求运行时派发被声明方法的关键字,并同时将该方法声明为重写(除了实现纯虚拟方法)。被声明的方法以及从该类向下继承层次结构中共享精确签名和名称的任何方法都是重写。当你通过父指针或引用调用虚拟方法时,运行时会调用被调用对象在继承层次结构中最具体的重写
当一个方法不是虚拟的,并且相同的方法稍后在继承层次结构中定义时,你正在隐藏父方法。区别在于,当通过基础指针或引用调用方法时,它将调用基础实现,而如果在派生对象中调用,则将调用派生实现。这是因为基本和派生函数是不相关的,如果在派生类中定义它,将隐藏来自调用的基础版本:
struct base {
   virtual void override() { std::cout << "base::override" << std::endl; }
   void not_override() { std::cout << "base::not_override" << std::endl; }
};
struct derived : base {
   void override() { std::cout << "derived::override" << std::endl; }
   void not_override() { std::cout << "derived::not_override" << std::endl; }
};
int main() {
   derived d;
   base & b = d;

   b.override();     // derived::override
   b.not_override(); // base::not_override
   d.not_override(); // derived::not_override
}

区别在于,@erik2red的答案中存在问题,因为覆盖与虚函数密切相关,并且意味着存在一个运行时分派机制来确定调用的最终派生类覆盖。而答案中展示的与覆盖相关的行为实际上是没有覆盖而是方法隐藏的行为。 其他问题 语言允许带有实现的纯虚拟方法。它并没有说明应该使用什么术语,但是纯虚拟方法永远不会被考虑用于运行时分派。原因是当一个带有纯虚拟方法(即使已经实现)的类被认为是抽象类,你不能实例化该类的对象。一旦你有了提供该方法实现的派生类,该实现就成为层次结构中的最终覆盖。该类现在可以被实例化,但是纯虚拟方法不会通过运行时分派机制被调用。
不是最终覆盖的虚拟方法以及隐藏的方法可以通过完全限定名称进行调用。对于虚拟方法,使用完全限定名称禁用了调用的多态分派机制:d.base::override()将调用基本实现,即使在派生类中存在其他覆盖
一个方法可以隐藏基类中的其他方法,即使签名不匹配。
struct base {
   void f() {}
};
struct derived : base {
   void f(int) {}
};
int main() {
   derived d;
   // d.f() // error, derived::f requires an argument, base::f is hidden in this context
}

与覆盖相同,d.base::f()会调用基本版本,并不是因为它禁用了多态性 - 它并没有,因为该方法未声明为虚拟方法,因此永远不会具有多态行为 - 而是因为完全限定名告诉编译器方法在哪里,即使它被派生类中的另一个方法隐藏。

我大约需要一个小时才能回答您,我不反对您,但您给“override”这个词赋予了严格的含义,这可能只对您有意义,而对于初学者则不然。顺便说一句,我们不应该在这里打这场争斗。这对任何人都没有帮助,只会让我们(双方)心满意足。 - erick2red
2
“override”这个术语在标准中有明确定义(10.3 [class.virtual]/2),因此具有严格的含义。初学者(和非初学者)最好学习这些术语的标准含义,因为这既是解释标准的工具,也是与其他程序员交流的语言。阅读您的评论(参与了这场争论),如果您发现评论或答案中有任何冒犯之处,我要道歉,那绝不是我的本意。 - David Rodríguez - dribeas
我同意这里的观点,没有比与那些对词语的理解不同于你的人交谈更无效率的事情了... 在最糟糕的情况下,你可能会满意地结束讨论,但后来发现他们理解的和你想的完全不一样... 因此,对于初学者来说,尽早学习正确术语是最好的选择,因为一旦形成习惯,就很难改变。 - Matthieu M.

3

摘要

本文讨论了C++中的虚函数。第零部分解释了如何声明和重载虚函数。第一部分试图(也许会失败)解释虚函数的实现方式。第二部分是一个使用在第零部分和第一部分定义的示例类的程序。第三部分是每个虚函数-多态性教程中都提供的经典动物示例。

第零部分

如果一个类的方法被声明为 virtual,那么它就是虚函数。

class my_base
{
public:
            void non_virtual_test() { cout << 4 << endl; } // non-virtual
    virtual void virtual_test()     { cout << 5 << endl; } // virtual
};

(当然,我假设程序员之前没有做过像#define virtual这样的事情。)
重新声明并重新实现其基类的非虚方法的类被称为重载该方法的类。重新声明并重新实现其基类的虚方法的类被称为覆盖该方法的类。
class my_derived : public my_base
{
public:
    void non_virtual_test() { cout << 6 << endl; } // overloaded
    void virtual_test()     { cout << 7 << endl; } // overriden
};

第一部分

当编译器检测到一个类有虚方法时,它会自动将虚方法表(也称为vtable)添加到类的内存布局中。这个结果类似于从编译以下代码生成的结果:

class my_base
{
//<vtable>
// The vtable is actually a bunch of member function pointers
protected:
    void (my_base::*virtual_test_ptr)();
//</vtable>

// The actual implementation of the virtual function
// is hidden from the rest of the program.
private:
    void virtual_test_impl() { cout << 5 << endl; }

// Initializing the real_virtual_test pointer in the vtable.
public:
    my_base() : virtual_test_ptr(&my_base::virtual_test_impl) {}

public:
    void non_virtual_test() { cout << 4 << endl; }
    // The interface of the virtual function is a wrapper
    // around the member function pointer.
    inline void virtual_test() { *virtual_test_ptr(); }
};

当编译器检测到一个类已经覆盖了一个虚方法时,它会替换在vtable中与之关联的条目。这个结果类似于从编译以下代码生成的结果:

class my_derived : public my_base
{
// The actual implementation of the virtual function
// is hidden from the rest of the program.
private:
    void virtual_test_impl() { cout << 7 << endl; }

// Initializing the real_virtual_test pointer in the vtable.
public:
    my_derived() : virtual_test_ptr(&my_derived::virtual_test_impl) {}

public:
    void non_virtual_test() { cout << 6 << endl; }
};

第二部分

现在清楚了虚函数是使用vtable实现的,而vtable仅仅是一堆函数指针,那么这段代码的作用就很明显了:

#include <iostream>

using namespace std;

class my_base
{
    public:
            void non_virtual_test() { cout << 4 << endl; }
    virtual void virtual_test()     { cout << 5 << endl; }
};

class my_derived : public my_base
{
public:
    void non_virtual_test() { cout << 6 << endl; }
    void virtual_test()     { cout << 7 << endl; }
}

int main()
{
    my_base* base_obj = new my_derived();

    // This outputs 4, since my_base::non_virtual_test() gets called,
    // not my_derived::non_virtual_test().
    base_obj->non_virtual_test();

    // This outputs 7, since the vtable pointer points to
    // my_derived::virtual_test(), not to my_base::virtual_test().
    base_obj->virtual_test();

    // We shall not forget
    // there was an object that was pointed by base_obj
    // who happily lived in the heap
    // until we killed it.
    delete base_obj;

    return 0;
}

第三部分

由于没有一个虚函数的例子是完整的,没有动物的例子...

#include <iostream>

using namespace std;

class animal
{
public:
    virtual void say_something()
    { cout << "I don't know what to say." << endl
           << "Let's assume I can growl." << endl; }

    /* A more sophisticated version would use pure virtual functions:
     *
     * virtual void say_something() = 0;
     */
};

class dog : public animal
{
public:
    void say_something() { cout << "Barf, barf..." << endl; }
};

class cat : public animal
{
public:
    void say_something() { cout << "Meow, meow..." << endl; }
};

int main()
{
    animal *a1 = new dog();
    animal *a2 = new cat();
    a1->say_something();
    a2->say_something();
}

1

从Java转到C++时,人们可能会觉得虚拟成员函数和非虚拟成员函数的概念令人困惑。要记住的是,Java方法对应于C++中的虚拟成员函数。

问题不在于我们为什么需要虚拟函数,而在于为什么需要非虚拟函数?我自己的理解(如果我错了,请纠正我)是它们更便宜实现,因为对它们的调用可以在编译时解析。


然而,在Java中有一些“final”方法是不打算被覆盖的。 - David Rodríguez - dribeas
选择C++的原因是因为设计一个可继承的类需要付出一定的努力,特别是因为没有根类(想想Java中的Object)。如果设计者没有明确决定使一个类可继承,那么就不应该从它继承。 - Joel

0

虚函数存在是为了帮助设计基类的行为。一个只有纯虚函数的基类不能被实例化,被称为抽象类。

由派生类来实现基类中由虚函数描述的方法。 派生类可以被实例化(它们存在并占用内存)。

从派生类派生可以重新定义在父对象中已经定义的函数。这个你已经知道的技巧叫做重载,它允许你自定义这个子对象的行为。

随着你学习更多C++,你会发现继承并不像它被吹嘘的那样好。组合通常是更好的选择。玩得开心。


0
经典的例子是绘图程序,其中创建一个带有虚拟draw()函数的基础Shape类。然后,可以创建每个形状(圆形、矩形、三角形等)作为子类,它们分别以适当的方式实现其draw()函数,核心绘图程序可以维护一个Shape列表,每个列表项都将执行相应的draw()函数,即使只存储了指向基础Shape类的指针。

-1

区别只在于当您通过基类对象的指针调用派生类的方法时。在那一刻,如果您调用的方法在派生类中被覆盖,您将得到基类的执行,而如果是虚拟的,则会执行派生类的方法。

#include <iostream>

class A{
    public:
    virtual void getA() { std::cout << "A in base" << std::endl;};
};

class B : public A {
    public:
    void getA() { std::cout << "A in derived class" << std::endl;}
};

int main(int argc, char** argv)
{
    A a;
    B b;
    a.getA();
    b.getA();

    A* t = new B;
    t->getA();
}

例如:在这个程序中,t->getA() 打印出 "派生类中的 A",但如果基类 A 中没有虚拟修饰符,则会打印出 "基类中的 A"
希望这能帮到你。

4
标准清晰地将你示例中的两个“getA”方法定义为“覆盖”。在基类中,虚函数和派生类中对同一函数的所有定义都是“覆盖”,它们为多态性定义了基础构件。当一个方法不是虚函数时,你不能对其进行覆盖,但可以通过在派生类中定义相同的方法来隐藏它,但这些方法不会“覆盖”基类方法。 - David Rodríguez - dribeas
2
@Martin York:这个答案错误地将“override”与“redefine”混淆了。只有虚函数才能被“override”。句子:“如果你调用的方法在派生类中被override,你将执行基类的操作”将非虚函数的非overrides行为与override术语相关联。当存在overrides时,实现将调用最终派生的override,而不是基类的。我想我的其他评论没有清楚解释我为什么会投反对票。 - David Rodríguez - dribeas
1
@david 你知道你的解释是文字游戏吗?我在这里考虑的非常实际,你可以称之为概念性,但重点仍然是相同的。只有非常高级的C++开发人员才能像你说的那样讲述“override”和“hiding”的真正区别。而且,一切都取决于非常严格的语言概念的C++实现,所以我不认为我的答案会误导任何人。当然,没有人会试图用如此简单的例子来解释如此大而广泛的概念。试着理解一下提问者。 - erick2red
1
问题是你完全弄错了'override'部分。派生类中的虚函数“覆盖”了基础方法。在上述引用行中,您写的行为不是“覆盖”的行为,而是非“覆盖”的行为。由于评论空间有限,无法更好地解释,因此我将在此“假答案”中继续进行讨论:https://dev59.com/mnE95IYBdhLWcg3wmvGh#2243224 - David Rodríguez - dribeas
尽管单词之间的差异很蠢,但这个答案实际上以一种非常易懂的方式解释了可适用的差异。 - Adrian Maire

-1

直升机和飞机都能飞行,但它们的飞行方式不同——它们都是某个假想对象“飞行器”的实例。您可以要求“飞行器”对象“飞行”,但“飞行器”只是一个接口,它除了应该能够飞行之外,不知道任何关于飞行的事情。

然而,如果直升机和飞机都遵循“飞行器”的接口,那么如果有一个机场对象并且您给它一个“飞行器”,所有机场需要做的就是请求“飞行器”飞行。

例如:

Airplace X=Airplane X("boeing 747");
Airfield::takeoff(&X);

Helicopter Y= Helicopter("Comache");
Airfield::takeof(&Y);

void Airfield::takeOff(Flyer * f)
{
     f->fly();
}

C++是一种严格的类型安全语言,只有在启用对象层次结构的RTTI时,通过基类间接调用派生类的函数才有可能实现此类功能,并且限定成员函数为虚函数可以实现这一点。


2
@Hassan:在大多数人想到C++ RTTI时所指的意义上(即typeinfo运算符和dynamic_cast所需的信息),RTTI并不是虚方法分派所必需的。通常,这只需要为每个类生成一个vtable和一个类实例的vtable指针。这就是RTTI,但如果你明白我的意思,它并不是“RTTI”。(免责声明:以上所有内容适用于大多数编译器,但并不是ISO标准规定的。) - Derrick Turk

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接