12

我是 C++ 新手。

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

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

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

4

10 回答 10

12

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

想一想具有说话功能的基类哺乳动物。该功能是无效的,只是简单地计算哺乳动物的说话方式。当你从这个类继承时,你可以重写 speak 方法,这样狗就会“Arf Arf!” 和猫去“喵喵”。

您的问题似乎在问有什么区别,没有区别,因为使用虚函数可以覆盖这些函数的行为。您可能会在重载函数和重载函数之间有所不同。

重载函数意味着创建一个名称相同但参数不同的函数,即不同数量和类型的参数。以下是IBM 网站上关于 C++ 重载的解释:

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

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

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

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

于 2010-02-10T17:35:47.397 回答
9

函数覆盖和函数之间的区别在多态virtual中变得很重要。特别是在使用指向基类的引用或指针时。

基本设置

在 C++ 中,任何派生类都可以传递给需要基类对象的函数。(另见切片LSP)。鉴于:

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

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

声明了两个函数,它们需要指向各自基类的指针。

派生类

现在让我们测试多态性,尤其是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++ 语言,我可以传递指向 a 的指针,Derived_From_Virtual因为Function_A继承Derived_From_VirtualBase_Virtual. 我也可以传递一个指向 to 的Derived_From_Nonvirtual指针Function_B

virtual和压倒一切的区别

virtual修饰符 inBase_Virtual告诉编译器将Function_A使用Derived_From_Virtual::some_virtual_function()in 代替方法Base_Virtual。这是因为该方法是virtual,最终定义可能驻留在未来派生类中。实际定义说要在包含该定义的最派生类中使用该方法。

当传递指向 的指针时Derived_From_NonvirtualFunction_B编译器将指示函数使用基类的方法Base_Nonvirtual::some_function(). 派生类中的some_function()方法是与基类分开的、不相关的方法。

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

于 2010-02-10T20:25:45.193 回答
8

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

在我学习 C++ 时,我个人发现 C++ FAQ 是一个很好的资源。其他人有不同意见,你的里程可能会有所不同

于 2010-02-10T17:38:54.427 回答
4

这更像是对这个答案的评论的跟进,而不是一个答案本身。

virtual是一个关键字,它为正在声明的方法请求运行时调度,同时将该方法声明为覆盖之一(除了实现的纯虚拟方法)。被声明的方法,以及从此类向下的派生层次结构中共享确切签名和名称的任何方法都是overrides。当您通过父指针或引用调用虚拟方法时,运行时将调用被调用对象的层次结构中最派生的覆盖

当方法不是虚拟的,并且稍后在层次结构中定义了相同的方法时,您将隐藏父方法。这里的区别在于,当通过基指针或引用调用方法时,它将调用基实现,而如果在派生对象中调用它,它将调用派生实现。在其他情况下,这被称为隐藏,因为基函数和派生函数是不相关的,并且在派生类中定义它会隐藏基版本的调用:

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
}

overrides一样,d.base::f()将调用基本版本,不是因为它禁用多态性——它没有,因为该方法未声明为 virtual 它永远不会具有多态行为——而是因为完整的限定告诉编译器该方法在哪里,甚至如果它被派生类中的另一个方法隐藏。

于 2010-02-11T08:48:51.473 回答
3

抽象的

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

零部分

当且仅当类的方法被声明为虚拟时,才称其为虚拟方法。

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

第二部分

现在很清楚虚函数是使用 vtables 实现的,它们只不过是一堆函数指针,应该清楚这段代码的作用:

#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();
}
于 2010-02-11T02:31:32.103 回答
1

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

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

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

随着您对 C++ 的了解越来越多,您会发现继承并不像人们想象的那样。作曲通常是更好的选择。玩得开心。

于 2010-02-10T18:05:14.717 回答
1

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

问题不在于为什么我们实际上有虚函数,而是为什么我们有非虚函数?我为自己证明它们的方式(如果我错了,请纠正我)是它们实现起来更便宜,因为对它们的调用可以在编译时解决。

于 2010-02-10T18:55:11.493 回答
0

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

于 2010-02-10T23:21:52.427 回答
-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()print "A in derived class",但是如果基类 A 中没有虚拟修饰符,那么它将打印"A in base"

希望能帮助到你。

于 2010-02-10T17:42:51.423 回答
-1

直升机和飞机都飞行,但它们以不同的方式飞行——它们都是一些假设对象 Flyer 的实例。您可以要求 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 时才可能实现。

于 2010-02-10T17:51:29.423 回答