为什么我们实际上有虚函数?

时间:2010-02-10 17:27:35

标签: c++ override virtual-functions redefinition

我是C ++的新手。

有人能告诉我c ++中方法覆盖和虚函数概念之间的区别。

虚拟函数的功能可以在其派生类中被覆盖。 在派生类中重新定义函数称为函数重写。

为什么我们实际上有虚函数?

10 个答案:

答案 0 :(得分:12)

虚函数/方法只是一个函数,其行为可以通过重新定义函数的工作方式(使用相同的签名)在子类(或C ++术语,派生类)中覆盖。

想想具有说话功能的基类哺乳动物。这个功能是无效的,只是简单地讲述哺乳动物的说话方式。当你从这个类继承时,你可以覆盖说话方法,这样狗就可以去“Arf Arf!”和猫去“喵喵”。

你的问题好像在问什么是差异,没有,因为虚函数可以覆盖这些函数的行为。你可能会追随重写函数之间的区别并重载它们。

重载函数意味着创建一个具有相同名称但不同参数的函数,即不同数量和类型的参数。以下是IBM's site中C ++重载的解释:

  

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

     

重载声明是已声明的声明   与同一范围内先前声明的声明的名称相同,   除了两个声明都有不同的类型。

     

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

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

答案 1 :(得分: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);

在上面的代码中,有两个基类,一个声明一个虚方法,另一个声明一个非虚函数。

声明了两个需要指向相应基本clases的函数。

派生类

现在让我们测试多态性,尤其是virtual与非虚拟(覆盖方法)。 结构:

struct Derived_From_Virtual
: public Base_Virtual
{
  void some_virtual_function(); // overrides Base_Virtual::some_virtual_function()
};

struct Derived_From_Nonvirtual :public Base_Nonvirtual {   void some_function(); }

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

virtual与覆盖

之间的差异

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

将指针传递给Derived_From_NonvirtualFunction_B时,编译器将指示函数使用基类Base_Nonvirtual::some_function()的方法。派生类中的some_function()方法是与基类不同的单独方法。

virtual和覆盖之间的主要区别在于多态性。

答案 2 :(得分:8)

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

我个人发现C ++ FAQ是我学习C ++的绝佳来源。其他人有不同的意见,你的里程可能会有所不同

答案 3 :(得分:4)

这更多是对此answer的评论的跟进,而不是单独的答案。

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在答案中的不同之处在于覆盖与虚函数密切相关,并暗示有一个运行时调度机制可以确定最多派生覆盖调用。答案中显示的与覆盖相关联的行为实际上是没有覆盖而是隐藏方法时的行为。

其他问题

该语言允许纯虚拟方法实现。它没有说明应该使用哪些术语,但是永远不会考虑运行时调度的纯虚方法。原因是当具有纯虚方法的类(即使实现)被认为是抽象类时,您无法实例化该类的对象。一旦有了为该方法提供实现的派生类,该实现就成为层次结构中的 final override 。现在可以实例化该类,但不会通过运行时调度机制调用纯虚方法。

如果使用完全限定名称,则可以调用非最终覆盖的虚拟方法以及隐藏方法。对于虚方法,使用完全限定名称会禁用调用的多态调度机制: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()将调用基本版本,而不是因为它禁用了多态 - 但是没有,因为该方法未被声明为虚拟,它将永远不会具有多态行为 - - 但是因为完整的限定条件告诉编译器方法在哪里,即使它被派生类中的另一个方法隐藏了。

答案 4 :(得分:2)

<强>摘要

在本文中,我们将讨论C ++中的虚函数。第零部分解释了如何声明和覆盖虚函数。第一部分尝试(也许失败)解释虚函数的实现方式。第二部分是一个示例程序,它使用零件和零件中定义的示例类。第三部分是每个虚函数中给出的经典动物示例 - 多态性教程。

PART ZERO

一个类的方法被称为虚拟当且仅当它被声明为是。

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();
}

答案 5 :(得分:1)

存在虚函数来帮助设计基类的行为。纯虚函数的基类无法实例化,称为抽象类。

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

从派生类派生可以重新定义已在父对象中定义的函数。您已经知道这种覆盖技术,并允许您自定义此子对象的行为。

当你学习更多C ++时,你会发现继承并不是它所能解决的问题。组成并且通常是更好的选择。玩得开心。

答案 6 :(得分:1)

当来自Java时,人们可能会发现虚拟成员函数与非虚拟成员函数的概念令人困惑。要记住的是Java方法对应于C ++中的虚拟成员函数。

问题不在于为什么我们实际上有虚拟功能,但为什么我们有非虚拟功能呢?我为自己辩护的方式(如果我错了,请纠正我)是因为它们的实现成本更低,因为对它们的调用可以在编译时解决。

答案 7 :(得分:0)

经典示例是一个绘图程序,其中使用虚拟draw()函数创建基础Shape类。然后可以将每个形状(圆形,矩形,三角形等)创建为子类,每个形状以适当的方式实现其draw()函数,并且核心绘制程序可以保留每个都进行适当绘制的形状列表( )函数即使只存储了指向基本Shape类的指针。

答案 8 :(得分:-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 in derived class",但如果基类A中没有虚拟修改器,则会打印"A in base"

希望它有所帮助。

答案 9 :(得分:-1)

直升飞机和飞机都飞行,但他们以不同的方式进行飞行 - 它们都是一些假想物体飞行物的实例。你可以要求Flyer对象“飞行” - 但是Flyer只是一个界面除了应该能够飞行之外,它对飞行没有任何了解。

然而,如果直升机和飞机都遵循飞行器的界面,那么如果有一个机场物体而你给它一个飞行器,那么所有机场需要做的就是要求飞行员飞行。

例如:

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时才有可能,并且限定成员函数virtual会启用此功能