ChatGPT解决这个技术问题 Extra ChatGPT

为什么我们需要 C++ 中的虚函数?

我正在学习 C++,我刚刚进入虚拟函数。

从我读到的(在书中和在线)中,虚函数是基类中的函数,您可以在派生类中覆盖它们。

但是在本书的前面部分,当学习基本继承时,我能够在不使用 virtual 的情况下覆盖派生类中的基函数。

那么我在这里错过了什么?我知道虚函数还有更多,而且它似乎很重要,所以我想弄清楚它到底是什么。我只是无法在网上找到一个简单的答案。

这可能是虚函数的最大好处——能够以这样一种方式构建代码,新派生类将自动使用旧代码而无需修改!
tbh,虚函数是 OOP 的主要特征,用于类型擦除。我认为,非虚拟方法使 Object Pascal 和 C++ 变得特别,优化了不必要的大 vtable 并允许与 POD 兼容的类。许多 OOP 语言期望每个方法都可以被覆盖。
这是一个很好的问题。事实上,C++ 中的这个虚拟事物在 Java 或 PHP 等其他语言中被抽象掉了。在 C++ 中,您只需对一些罕见的情况(注意多重继承或 DDOD 的特殊情况)获得更多控制权。但是为什么这个问题会发布在 stackoverflow.com 上?
我认为如果你看一下 early binding-late binding 和 VTABLE 会更合理和有意义。所以这里有一个很好的解释( learncpp.com/cpp-tutorial/125-the-virtual-table )。
我还没有找到比这更好的教程:nrecursions.blogspot.com/2015/06/…

N
Neuron

以下是我不仅了解 virtual 函数是什么,还了解为什么需要它们的方式:

假设你有这两个类:

class Animal
{
    public:
        void eat() { std::cout << "I'm eating generic food."; }
};

class Cat : public Animal
{
    public:
        void eat() { std::cout << "I'm eating a rat."; }
};

在您的主要功能中:

Animal *animal = new Animal;
Cat *cat = new Cat;

animal->eat(); // Outputs: "I'm eating generic food."
cat->eat();    // Outputs: "I'm eating a rat."

到目前为止一切顺利,对吧?动物吃普通食物,猫吃老鼠,都没有virtual

现在让我们稍微改变一下,以便通过中间函数调用 eat()(对于这个例子来说是一个简单的函数):

// This can go at the top of the main.cpp file
void func(Animal *xyz) { xyz->eat(); }

现在我们的主要功能是:

Animal *animal = new Animal;
Cat *cat = new Cat;

func(animal); // Outputs: "I'm eating generic food."
func(cat);    // Outputs: "I'm eating generic food."

哦哦……我们把一只猫传给了func(),但它不会吃老鼠。您是否应该重载 func() 以使其需要 Cat*?如果您必须从 Animal 派生更多动物,它们都需要自己的 func()

解决方案是将 Animal 类中的 eat() 设为虚函数:

class Animal
{
    public:
        virtual void eat() { std::cout << "I'm eating generic food."; }
};

class Cat : public Animal
{
    public:
        void eat() { std::cout << "I'm eating a rat."; }
};

主要的:

func(animal); // Outputs: "I'm eating generic food."
func(cat);    // Outputs: "I'm eating a rat."

完毕。


因此,如果我正确理解这一点, virtual 允许调用子类方法,即使对象被视为其超类?
这里没有通过中间函数“func”的例子来解释后期绑定,这里有一个更直接的演示——Animal *animal = new Animal; //猫 *cat = 新猫;动物 *cat = 新猫;动物->吃(); // 输出:“我正在吃普通食物。”猫->吃(); // 输出:“我正在吃普通食物。”即使您正在分配子类对象 (Cat),被调用的方法也是基于指针类型 (Animal) 而不是它指向的对象类型。这就是您需要“虚拟”的原因。
我是唯一一个在 C++ 中发现这种默认行为很奇怪的人吗?我本来希望没有“虚拟”的代码可以工作。
@David天宇Wong我认为virtual引入了一些动态绑定与静态绑定,是的,如果您来自Java之类的语言,这很奇怪。
首先,虚拟调用比常规函数调用要昂贵得多。默认情况下,C++ 哲学很快,因此默认情况下虚拟调用是一个很大的禁忌。第二个原因是,如果您从库中继承一个类并且它更改了公共或私有方法的内部实现(在内部调用虚拟方法)而不更改基类行为,那么虚拟调用可能会导致您的代码中断。
佚名

没有“虚拟”,您将获得“早期绑定”。使用该方法的哪个实现在编译时根据您调用的指针的类型来决定。

使用“虚拟”,您将获得“后期绑定”。使用该方法的哪个实现在运行时根据所指向对象的类型来决定——它最初被构造为什么。根据指向该对象的指针的类型,这不一定是您所想的。

class Base
{
  public:
            void Method1 ()  {  std::cout << "Base::Method1" << std::endl;  }
    virtual void Method2 ()  {  std::cout << "Base::Method2" << std::endl;  }
};

class Derived : public Base
{
  public:
    void Method1 ()  {  std::cout << "Derived::Method1" << std::endl;  }
    void Method2 ()  {  std::cout << "Derived::Method2" << std::endl;  }
};

Base* basePtr = new Derived ();
  //  Note - constructed as Derived, but pointer stored as Base*

basePtr->Method1 ();  //  Prints "Base::Method1"
basePtr->Method2 ();  //  Prints "Derived::Method2"

编辑 - 参见this question

此外 - this tutorial 涵盖了 C++ 中的早期和晚期绑定。


非常好,并且使用更好的示例快速回家。然而,这很简单,提问者应该只阅读页面 parashift.com/c++-faq-lite/virtual-functions.html。其他人已经在从这个线程链接的 SO 文章中指出了这个资源,但我相信这值得重新提及。
我不知道早期绑定和后期绑定是否是 c++ 社区中专门使用的术语,但正确的术语是静态(在编译时)和动态(在运行时)绑定。
@mike - "The term "late binding" dates back to at least the 1960s, where it can be found in Communications of the ACM."。如果每个概念都有一个正确的词,那不是很好吗?不幸的是,事实并非如此。术语“早期绑定”和“后期绑定”早于 C++ 甚至面向对象编程,并且与您使用的术语一样正确。
@BJovke - 这个答案是在 C++11 发布之前写的。即便如此,我只是在 GCC 6.3.0 中编译它(默认使用 C++14)没有问题 - 显然包装变量声明并调用 main 函数等。指向派生的 隐式< /i> 转换为指向基址的指针(更专业的隐式转换为更一般的)。反之亦然,您需要显式转换,通常是 dynamic_cast。其他任何事情 - 很容易出现未定义的行为,因此请确保您知道自己在做什么。据我所知,这在 C++98 之前就没有改变过。
请注意,今天的 C++ 编译器通常可以在后期优化到早期绑定——当它们可以确定绑定将是什么时。这也称为“去虚拟化”。
c
cdhowie

您需要至少 1 级继承和向上转换来证明它。这是一个非常简单的例子:

class Animal
{        
    public: 
      // turn the following virtual modifier on/off to see what happens
      //virtual   
      std::string Says() { return "?"; }  
};

class Dog: public Animal
{
    public: std::string Says() { return "Woof"; }
};

void test()
{
    Dog* d = new Dog();
    Animal* a = d;       // refer to Dog instance with Animal pointer

    std::cout << d->Says();   // always Woof
    std::cout << a->Says();   // Woof or ?, depends on virtual
}

你的例子说返回的字符串取决于函数是否是虚拟的,但它没有说明哪个结果对应于虚拟,哪个对应于非虚拟。此外,这有点令人困惑,因为您没有使用返回的字符串。
带有虚拟关键字:Woof。没有虚拟关键字:?。
@HeshamEraqi 没有虚拟它是早期绑定,它将显示“?”基类
Y
Yun

虚函数用于支持运行时多态性。

也就是说,virtual 关键字告诉编译器不要在编译时做出(函数绑定的)决定,而是将它推迟到运行时”。

您可以通过在其基类声明中添加关键字 virtual 来使函数成为虚拟函数。例如,类 Base { virtual void func(); }

当基类具有虚成员函数时,任何从基类继承的类都可以重新定义具有完全相同原型的函数,即只能重新定义功能,不能重新定义函数的接口。类派生:公共基础{无效函数(); }

基类指针可用于指向基类对象以及派生类对象。

当使用基类指针调用虚函数时,编译器在运行时决定调用哪个版本的函数——即基类版本或重写的派生类版本。这称为运行时多态性。


S
Samuel Sorial

您需要虚拟方法来实现安全向下转换、简单和简洁。

这就是虚拟方法所做的:它们安全地向下转换,代码显然简单而简洁,避免了不安全的手动转换,否则您将拥有更复杂和冗长的代码。

以下代码故意“不正确”。它没有将 value 方法声明为 virtual,因此会产生一个意想不到的“错误”结果,即 0:

#include <iostream>
using namespace std;

class Expression
{
public:
    auto value() const
        -> double
    { return 0.0; }         // This should never be invoked, really.
};

class Number
    : public Expression
{
private:
    double  number_;
    
public:
    auto value() const
        -> double
    { return number_; }     // This is OK.

    Number( double const number )
        : Expression()
        , number_( number )
    {}
};

class Sum
    : public Expression
{
private:
    Expression const*   a_;
    Expression const*   b_;
    
public:
    auto value() const
        -> double
    { return a_->value() + b_->value(); }       // Uhm, bad! Very bad!

    Sum( Expression const* const a, Expression const* const b )
        : Expression()
        , a_( a )
        , b_( b )
    {}
};

auto main() -> int
{
    Number const    a( 3.14 );
    Number const    b( 2.72 );
    Number const    c( 1.0 );

    Sum const       sum_ab( &a, &b );
    Sum const       sum( &sum_ab, &c );
    
    cout << sum.value() << endl;
}

在注释为“坏”的行中,调用了 Expression::value 方法,因为 静态已知类型(编译时已知的类型)是 Expression,而 value 方法不是虚拟的.

在静态已知类型 Expression 中将 value 声明为 virtual 可确保每次调用都会检查这是什么实际的对象类型,并为该 调用 value 的相关实现>动态类型

#include <iostream>
using namespace std;

class Expression
{
public:
    virtual
    auto value() const -> double
        = 0;
};

class Number
    : public Expression
{
private:
    double  number_;
    
public:
    auto value() const -> double
        override
    { return number_; }

    Number( double const number )
        : Expression()
        , number_( number )
    {}
};

class Sum
    : public Expression
{
private:
    Expression const*   a_;
    Expression const*   b_;
    
public:
    auto value() const -> double
        override
    { return a_->value() + b_->value(); }    // Dynamic binding, OK!

    Sum( Expression const* const a, Expression const* const b )
        : Expression()
        , a_( a )
        , b_( b )
    {}
};

auto main() -> int
{
    Number const    a( 3.14 );
    Number const    b( 2.72 );
    Number const    c( 1.0 );

    Sum const       sum_ab( &a, &b );
    Sum const       sum( &sum_ab, &c );
    
    cout << sum.value() << endl;
}

这里的输出应该是 6.86,因为虚拟方法被虚拟调用。这也称为调用的动态绑定。执行一点检查,找到对象的实际动态类型,并调用该动态类型的相关方法实现。

相关实现是最具体(最派生)类中的实现。

请注意,此处派生类中的方法实现未标记为 virtual,而是标记为 override。它们可以标记为 virtual,但它们自动是虚拟的。 override 关键字确保如果在某些基类中 存在这样的虚拟方法,那么您将得到一个错误(这是可取的)。

如果没有 virtual,则必须实现一些 Do It Yourself 版本的动态绑定。这通常涉及不安全的手动向下转换、复杂性和冗长。

对于单个函数的情况,就像这里一样,将函数指针存储在对象中并通过该函数指针调用就足够了,但即便如此,它也会涉及一些不安全的向下转换、复杂性和冗长,即:

#include <iostream>
using namespace std;

class Expression
{
protected:
    typedef auto Value_func( Expression const* ) -> double;

    Value_func* value_func_;

public:
    auto value() const
        -> double
    { return value_func_( this ); }
    
    Expression(): value_func_( nullptr ) {}     // Like a pure virtual.
};

class Number
    : public Expression
{
private:
    double  number_;
    
    static
    auto specific_value_func( Expression const* expr )
        -> double
    { return static_cast<Number const*>( expr )->number_; }

public:
    Number( double const number )
        : Expression()
        , number_( number )
    { value_func_ = &Number::specific_value_func; }
};

class Sum
    : public Expression
{
private:
    Expression const*   a_;
    Expression const*   b_;
    
    static
    auto specific_value_func( Expression const* expr )
        -> double
    {
        auto const p_self  = static_cast<Sum const*>( expr );
        return p_self->a_->value() + p_self->b_->value();
    }

public:
    Sum( Expression const* const a, Expression const* const b )
        : Expression()
        , a_( a )
        , b_( b )
    { value_func_ = &Sum::specific_value_func; }
};


auto main() -> int
{
    Number const    a( 3.14 );
    Number const    b( 2.72 );
    Number const    c( 1.0 );

    Sum const       sum_ab( &a, &b );
    Sum const       sum( &sum_ab, &c );
    
    cout << sum.value() << endl;
}

看待这一点的一种积极方式是,如果您遇到上述不安全的向下转换、复杂性和冗长,那么通常一个或多个虚拟方法可以真正提供帮助。


P
P.W

如果基类是 Base,而派生类是 Der,则您可以有一个 Base *p 指针,它实际上指向 Der 的一个实例。当您调用 p->foo(); 时,如果 foo 不是虚拟的,那么 Base 的版本会执行,而忽略 p 实际上指向 Der 的事实。如果 foo 虚拟的,则 p->foo() 执行 foo 的“最叶”覆盖,充分考虑所指向项目的实际类。所以虚拟和非虚拟之间的区别实际上非常关键:前者允许运行时polymorphism,这是面向对象编程的核心概念,而后者不允许。


我不想反驳你,但编译时多态性仍然是多态性。甚至重载非成员函数也是一种多态性 - 使用链接中的术语的临时多态性。这里的区别在于早期绑定和晚期绑定。
@ Steve314,你是迂腐的正确(作为一个迂腐的伙伴,我同意这一点;-) - 编辑答案以添加缺失的形容词;-)。
N
Neuron

我想添加虚拟函数的另一种用法,尽管它使用与上述答案相同的概念,但我想它值得一提。

虚拟破坏者

考虑下面的这个程序,没有将基类析构函数声明为虚拟; Cat 的内存可能无法清理。

class Animal {
    public:
    ~Animal() {
        cout << "Deleting an Animal" << endl;
    }
};
class Cat:public Animal {
    public:
    ~Cat() {
        cout << "Deleting an Animal name Cat" << endl;
    }
};

int main() {
    Animal *a = new Cat();
    delete a;
    return 0;
}

输出:

删除动物

class Animal {
    public:
    virtual ~Animal() {
        cout << "Deleting an Animal" << endl;
    }
};
class Cat:public Animal {
    public:
    ~Cat(){
        cout << "Deleting an Animal name Cat" << endl;
    }
};

int main() {
    Animal *a = new Cat();
    delete a;
    return 0;
}

输出:

删除动物名称 猫 删除动物


without declaring Base class destructor as virtual; memory for Cat may not be cleaned up. 比这更糟。通过基指针/引用删除派生对象是纯粹的未定义行为。所以,不仅仅是一些内存可能泄漏。相反,该程序格式不正确,因此编译器可能会将其转换为任何东西:碰巧可以正常工作的机器代码,或者什么都不做,或者从你的鼻子里召唤恶魔,等等。这就是为什么,如果程序的设计方式是某些用户可能通过基引用删除派生实例,则基必须具有虚拟析构函数
A
Ajay GU

需要虚拟功能解释【通俗易懂】

#include<iostream>

using namespace std;

class A{
public: 
        void show(){
        cout << " Hello from Class A";
    }
};

class B :public A{
public:
     void show(){
        cout << " Hello from Class B";
    }
};


int main(){

    A *a1 = new B; // Create a base class pointer and assign address of derived object.
    a1->show();

}

输出将是:

Hello from Class A.

但使用虚函数:

#include<iostream>

using namespace std;

class A{
public:
    virtual void show(){
        cout << " Hello from Class A";
    }
};

class B :public A{
public:
    virtual void show(){
        cout << " Hello from Class B";
    }
};


int main(){

    A *a1 = new B;
    a1->show();

}

输出将是:

Hello from Class B.

因此,使用虚函数可以实现运行时多态性。


h
h0b0

您必须区分覆盖和重载。如果没有 virtual 关键字,您只能重载基类的方法。这意味着隐藏。假设您有一个基类 Base 和一个派生类 Specialized,它们都实现了 void foo()。现在您有一个指向 Base 的指针,该指针指向 Specialized 的一个实例。当您对其调用 foo() 时,您可以观察到 virtual 的不同之处:如果方法是虚拟的,将使用 Specialized 的实现,如果缺少它,将选择 Base 的版本。最好的做法是永远不要重载基类中的方法。使方法成为非虚拟方法是其作者告诉您其在子类中的扩展不是有意的方式。


如果没有 virtual,您就不会重载。你在遮蔽。如果基类 B 具有一个或多个函数 foo,并且派生类 D 定义了一个 foo 名称,则 foo 隐藏所有这些 foo-s在 B。使用范围解析将它们作为 B::foo 到达。要将 B::foo 函数提升为 D 以进行重载,您必须使用 using B::foo
“重载”基本上意味着您编写另一个函数,纯属巧合,它与现有函数具有相同的名称。
N
Neuron

为了更好地阅读,我以对话的形式给出了答案:

为什么我们需要虚函数?

因为多态。

什么是多态性?

基指针也可以指向派生类型对象这一事实。

这种多态性的定义如何导致对虚函数的需求?

好吧,通过早期绑定。

什么是早期绑定?

C++ 中的早期绑定(编译时绑定)是指在程序执行之前固定函数调用。

所以...?

因此,如果您使用基类型作为函数的参数,编译器将仅识别基接口,并且如果您使用派生类中的任何参数调用该函数,它将被切掉,这不是您想要发生的。

如果这不是我们想要发生的,为什么允许这样做?

因为我们需要多态!

那么多态有什么好处呢?

您可以使用基类型指针作为单个函数的参数,然后在程序的运行时,您可以访问每个派生类型接口(例如它们的成员函数)而没有任何问题,使用该单个函数的解引用基指针。

我还是不知道虚函数有什么用……!这是我的第一个问题!

好吧,这是因为你问得太早了!

为什么我们需要虚函数?

假设您使用基指针调用函数,该指针具有来自其派生类之一的对象的地址。正如我们上面所讨论的,在运行时,这个指针被取消引用,到目前为止一切都很好,但是,我们期望“来自我们的派生类”的方法(== 成员函数)被执行!但是,在基类中已经定义了相同的方法(具有相同标头的方法),那么您的程序为什么还要费心选择其他方法呢?换句话说,我的意思是,你怎么能把这种情况与我们过去经常看到的情况区分开来呢?

简短的回答是“基类中的一个虚拟成员函数”,稍长一点的回答是,“在这一步,如果程序在基类中看到一个虚函数,它就知道(意识到)你正在尝试使用多态性”,因此转到派生类(使用 v-table,一种后期绑定形式)以找到具有相同标头的 另一个方法,但 - 预期 - 具有不同的实现。

为什么不同的实现?

你这个笨蛋!去阅读good book

好吧,等等等等,当他/她可以简单地使用派生类型指针时,为什么还要费心使用基指针?你是法官,这一切的头痛值得吗?看看这两个片段:

//1:

Parent* p1 = &boy;
p1 -> task();
Parent* p2 = &girl;
p2 -> task();

//2:

Boy* p1 = &boy;
p1 -> task();
Girl* p2 = &girl;
p2 -> task();

好的,虽然我认为 1 仍然比 2 好,但你也可以这样写 1:

//1:

Parent* p1 = &boy;
p1 -> task();
p1 = &girl;
p1 -> task();

此外,您应该知道,这只是对我迄今为止向您解释的所有内容的人为使用。取而代之的是,假设您的程序中有一个函数分别使用每个派生类的方法(getMonthBenefit()):

double totalMonthBenefit = 0;    
std::vector<CentralShop*> mainShop = { &shop1, &shop2, &shop3, &shop4, &shop5, &shop6};
for(CentralShop* x : mainShop){
     totalMonthBenefit += x -> getMonthBenefit();
}

现在,尝试重新编写这个,不要头疼!

double totalMonthBenefit=0;
Shop1* branch1 = &shop1;
Shop2* branch2 = &shop2;
Shop3* branch3 = &shop3;
Shop4* branch4 = &shop4;
Shop5* branch5 = &shop5;
Shop6* branch6 = &shop6;
totalMonthBenefit += branch1 -> getMonthBenefit();
totalMonthBenefit += branch2 -> getMonthBenefit();
totalMonthBenefit += branch3 -> getMonthBenefit();
totalMonthBenefit += branch4 -> getMonthBenefit();
totalMonthBenefit += branch5 -> getMonthBenefit();
totalMonthBenefit += branch6 -> getMonthBenefit();

实际上,这也可能是一个人为的例子!


应该强调使用单个(超)对象类型迭代不同类型(子)对象的概念,这是你给出的一个好点,谢谢
Z
Ziezi

为什么我们需要 C++ 中的虚拟方法?

快速回答:

它为我们提供了面向对象编程所需的“成分”之一。

在 Bjarne Stroustrup C++ 编程:原理与实践,(14.3):

虚函数提供了在基类中定义函数并在用户调用基类函数时调用的派生类中具有相同名称和类型的函数的能力。这通常称为运行时多态性、动态分派或运行时分派,因为调用的函数是在运行时根据所使用对象的类型确定的。

如果您需要虚函数调用 2,这是最快更有效的实现。

处理一个虚调用,需要一个或多个与派生对象相关的数据 3.通常的做法是添加函数表的地址。该表通常称为虚表或虚函数表,其地址通常称为虚指针。每个虚函数在虚表中都有一个槽。根据调用者的对象(派生)类型,虚函数依次调用相应的覆盖。

1.继承、运行时多态性和封装的使用是面向对象编程最常见的定义。

2. 您无法将功能编码得更快或使用更少的内存使用其他语言功能在运行时选择备选方案。 Bjarne Stroustrup C++ 编程:原理与实践。(14.3.1)。

3. 当我们调用包含虚函数的基类时,告诉我们真正调用了哪个函数。


n
nitin_cherian

当您在基类中有函数时,您可以在派生类中RedefineOverride 它。

重新定义方法:派生类中给出了基类方法的新实现。 促进Dynamic binding

覆盖方法Redefining派生类中基类的virtual method。虚拟方法促进动态绑定

所以当你说:

但是在本书的前面,当学习基本继承时,我能够在不使用“虚拟”的情况下覆盖派生类中的基本方法。

您没有覆盖它,因为基类中的方法不是虚拟的,而是您正在重新定义它


E
Echilon

如果您了解底层机制,它会有所帮助。 C++ 形式化了 C 程序员使用的一些编码技术,用“覆盖”替换“类”——具有公共标题部分的结构将用于处理不同类型但具有一些公共数据或操作的对象。通常,覆盖层的基本结构(公共部分)有一个指向函数表的指针,该函数表指向每个对象类型的一组不同的例程。 C++ 做同样的事情,但隐藏了机制,即 C++ ptr->func(...),其中 func 是虚拟的,因为 C 是 (*ptr->func_table[func_num])(ptr,...),派生类之间的变化是 func_table 内容。 [非虚拟方法 ptr->func() 只是转换为 mangled_func(ptr,..)。]

这样做的结果是,您只需要了解基类即可调用派生类的方法,即如果例程了解类 A,您可以将派生类 B 指针传递给它,然后调用的虚拟方法将是那些的 B 而不是 A,因为您通过函数表 B 指向。


r
rvkreddy

关键字 virtual 告诉编译器它不应该执行早期绑定。相反,它应该自动安装执行后期绑定所需的所有机制。为此,典型的编译器 1 为每个包含虚函数的类创建一个表(称为 VTABLE)。编译器将特定类的虚函数地址放在 VTABLE 中。在每个带有虚函数的类中,它都会偷偷地放置一个指针,称为 vpointer(缩写为 VPTR),它指向该对象的 VTABLE。当您通过基类指针进行虚函数调用时,编译器会悄悄地插入代码以获取 VPTR 并在 VTABLE 中查找函数地址,从而调用正确的函数并导致后期绑定发生。

此链接中的更多详细信息 http://cplusplusinterviews.blogspot.sg/2015/04/virtual-mechanism.html


j
javaProgrammer

virtual 关键字强制编译器选择定义在对象类而不是指针类中的方法实现。

Shape *shape = new Triangle(); 
cout << shape->getName();

在上面的例子中,Shape::getName 将默认被调用,除非 getName() 在基类 Shape 中被定义为 virtual。这迫使编译器在 Triangle 类而不是 Shape 类中查找 getName() 实现。

虚拟表是编译器跟踪子类的各种虚拟方法实现的机制。这也称为动态调度,并且有一些与之相关的开销。

最后,为什么在 C++ 中甚至需要 virtual,为什么不像 Java 那样让它成为默认行为呢?

C++ 基于“零开销”和“按使用付费”的原则。所以它不会尝试为您执行动态调度,除非您需要它。为界面提供更多控制。通过使函数成为非虚拟函数,接口/抽象类可以控制其所有实现中的行为。


J
Jörg Brüggmann

OOP 答案:亚型多态性

在 C++ 中,需要虚拟方法来实现多态性,如果您应用维基百科的定义,更准确地说是子类型化或子类型多态性。

维基百科,子类型,2019-01-09:在编程语言理论中,子类型(也称为子类型多态性或包含多态性)是类型多态性的一种形式,其中子类型是通过某种概念与另一个数据类型(超类型)相关的数据类型可替代性,意味着程序元素,通常是子例程或函数,被编写为对超类型的元素进行操作,也可以对子类型的元素进行操作。

注意:子类型表示基类,子类型表示继承类。

关于亚型多态性的进一步阅读

https://en.wikipedia.org/wiki/Subtyping

https://en.wikipedia.org/wiki/Polymorphism_(computer_science)#Subtyping

技术答案:动态调度

如果您有一个指向基类的指针,那么该方法的调用(即声明为虚拟的)将被分派给所创建对象的实际类的方法。这就是 C++ 实现子类型多态的方式。

进一步阅读 C++ 中的多态性和动态调度

http://www.cplusplus.com/doc/tutorial/polymorphism/

https://en.cppreference.com/w/cpp/language/virtual

实施答案:创建 vtable 条目

对于方法上的每个修饰符“virtual”,C++ 编译器通常在声明该方法的类的 vtable 中创建一个条目。这就是普通 C++ 编译器实现动态调度的方式。

进一步阅读 vtables

https://en.wikipedia.org/wiki/Virtual_method_table

示例代码

#include <iostream>

using namespace std;

class Animal {
public:
    virtual void MakeTypicalNoise() = 0; // no implementation needed, for abstract classes
    virtual ~Animal(){};
};

class Cat : public Animal {
public:
    virtual void MakeTypicalNoise()
    {
        cout << "Meow!" << endl;
    }
};

class Dog : public Animal {
public:
    virtual void MakeTypicalNoise() { // needs to be virtual, if subtype polymorphism is also needed for Dogs
        cout << "Woof!" << endl;
    }
};

class Doberman : public Dog {
public:
    virtual void MakeTypicalNoise() {
        cout << "Woo, woo, woow!";
        cout << " ... ";
        Dog::MakeTypicalNoise();
    }
};

int main() {

    Animal* apObject[] = { new Cat(), new Dog(), new Doberman() };

    const   int cnAnimals = sizeof(apObject)/sizeof(Animal*);
    for ( int i = 0; i < cnAnimals; i++ ) {
        apObject[i]->MakeTypicalNoise();
    }
    for ( int i = 0; i < cnAnimals; i++ ) {
        delete apObject[i];
    }
    return 0;
}

示例代码的输出

Meow!
Woof!
Woo, woo, woow! ... Woof!

代码示例的UML类图

https://i.stack.imgur.com/Dprca.png


接受我的投票,因为您展示了多态性可能最重要的用途:具有虚成员函数的基类指定了一个接口,或者换句话说,一个 API。使用这样一个类框架的代码(这里:你的 main 函数)可以统一处理集合(这里:你的数组)中的所有项目,并且不需要、不想,而且通常不知道将调用哪个具体实现在运行时,例如因为它还不存在。这是在对象和处理程序之间建立抽象关系的基础之一。
N
Nav

对虚函数的解释的问题在于,它们没有解释它在实践中是如何使用的,以及它如何有助于可维护性。我创建了一个虚拟功能教程,人们已经发现它非常有用。此外,它以战场为前提,因此更令人兴奋:https://nrecursions.blogspot.com/2015/06/so-why-do-we-need-virtual-functions.html

https://i.stack.imgur.com/qMQ0Q.png

#include "iostream"

//This class is created by Gun1's company
class Gun1 {public: void fire() {std::cout<<"gun1 firing now\n";}};
//This class is created by Gun2's company
class Gun2 {public: void shoot() {std::cout<<"gun2 shooting now\n";}};

//We create an abstract class to interface with WeaponController
class WeaponsInterface {
 public:
 virtual void shootTarget() = 0;
};

//A wrapper class to encapsulate Gun1's shooting function
class WeaponGun1 : public WeaponsInterface {
 private:
 Gun1* g;

 public:
 WeaponGun1(): g(new Gun1()) {}
 ~WeaponGun1() { delete g;}
 virtual void shootTarget() { g->fire(); }
};

//A wrapper class to encapsulate Gun2's shooting function
class WeaponGun2 : public WeaponsInterface {
 private:
 Gun2* g;

 public:
 WeaponGun2(): g(new Gun2()) {}
 ~WeaponGun2() { delete g;}
 virtual void shootTarget() { g->shoot(); }
};

class WeaponController {
 private:
 WeaponsInterface* w;
 WeaponGun1* g1;
 WeaponGun2* g2;
 public:
 WeaponController() {g1 = new WeaponGun1(); g2 = new WeaponGun2(); w = g1;}
 ~WeaponController() {delete g1; delete g2;}
 void shootTarget() { w->shootTarget();}
 void changeGunTo(int gunNumber) {//Virtual functions makes it easy to change guns dynamically
   switch(gunNumber) {
     case 1: w = g1; break;
     case 2: w = g2; break;
   }
 }
};


class BattlefieldSoftware {
 private:
 WeaponController* wc;
 public:
 BattlefieldSoftware() : wc(new WeaponController()) {}
 ~BattlefieldSoftware() { delete wc; }

 void shootTarget() { wc->shootTarget(); }
 void changeGunTo(int gunNumber) {wc->changeGunTo(gunNumber); }
};


int main() {
 BattlefieldSoftware* bf = new BattlefieldSoftware();
 bf->shootTarget();
 for(int i = 2; i > 0; i--) {
     bf->changeGunTo(i);
     bf->shootTarget();
 }
 delete bf;
}

我鼓励您首先阅读博客上的帖子,以了解创建包装类的原因。

如图所示,有各种枪支/导弹可以连接到战场软件,并且可以向这些武器发出命令、开火或重新校准等。这里的挑战是能够改变/更换枪支/导弹无需更改蓝色战场软件,并且能够在运行期间在武器之间切换,无需更改代码并重新编译。

上面的代码显示了问题是如何解决的,以及具有设计良好的包装类的虚函数如何封装函数并帮助在运行时分配派生类指针。类 WeaponGun1 的创建确保您已将 Gun1 的处理完全分离到类中。无论您对 Gun1 进行何种更改,您只需在 WeaponGun1 中进行更改,并且确信不会影响其他类。

由于有 WeaponsInterface 类,您现在可以将任何派生类分配给基类指针 WeaponsInterface,并且因为它的函数是虚拟的,所以当您调用 WeaponsInterfaceshootTarget 时,将调用派生类 shootTarget

最好的部分是,您可以在运行时更换枪(w=g1w=g2)。这是虚函数的主要优点,这也是我们需要虚函数的原因。

所以在换枪时不再需要在各个地方注释掉代码。现在这是一个简单而干净的过程,并且添加更多的枪类也更容易,因为我们只需要创建一个新的 WeaponGun3WeaponGun4 类,我们可以确信它不会弄乱 BattlefieldSoftware 的代码或 WeaponGun1/WeaponGun2 的代码。


我也最喜欢这个。当我看到动物和狗的例子时,我有完全相同的想法,比如你为什么要这样做?这是理解虚函数的实用方法的更好示例。这一点我要提一下。原始博客文章中的第一个示例(动物和狗类)不完整。如果我在受伤之前没有阅读其他示例,我将无法理解它。
P
Peter Mortensen

为什么我们需要虚函数?

虚函数避免了不必要的类型转换问题,我们中的一些人可能会争论,当我们可以使用派生类指针调用派生类中特定的函数时,为什么我们需要虚函数!答案是 - 它使大型系统中继承的整个想法无效开发,其中非常需要具有单指针基类对象。

让我们比较下面两个简单的程序来理解虚函数的重要性:

没有虚函数的程序:

#include <iostream>
using namespace std;

class father
{
    public: void get_age() {cout << "Fathers age is 50 years" << endl;}
};

class son: public father
{
    public : void get_age() { cout << "son`s age is 26 years" << endl;}
};

int main(){
    father *p_father = new father;
    son *p_son = new son;

    p_father->get_age();
    p_father = p_son;
    p_father->get_age();
    p_son->get_age();
    return 0;
}

输出:

Fathers age is 50 years
Fathers age is 50 years
son`s age is 26 years

具有虚函数的程序:

#include <iostream>
using namespace std;

class father
{
    public:
        virtual void get_age() {cout << "Fathers age is 50 years" << endl;}
};

class son: public father
{
    public : void get_age() { cout << "son`s age is 26 years" << endl;}
};

int main(){
    father *p_father = new father;
    son *p_son = new son;

    p_father->get_age();
    p_father = p_son;
    p_father->get_age();
    p_son->get_age();
    return 0;
}

输出:

Fathers age is 50 years
son`s age is 26 years
son`s age is 26 years

通过仔细分析这两种输出,我们可以理解虚函数的重要性。


D
Duke

关于效率,虚函数的效率不如早期绑定函数。

“这种虚拟调用机制几乎可以与“普通函数调用”机制一样高效(在 25% 以内)。它的空间开销是具有虚拟函数的类的每个对象中的一个指针加上每个此类类的一个 vtbl”[A Bjarne Stroustrup 的 C++ 之旅]


后期绑定不仅会使函数调用变慢,还会使被调用的函数在运行时未知,因此无法应用跨函数调用的优化。这可以改变一切 f.ex。在值传播删除大量代码的情况下(想想if(param1>param2) return cst;,在某些情况下,编译器可以将整个函数调用减少为常量)。
佚名

虚拟方法用于界面设计。例如,在 Windows 中有一个名为 IUnknown 的接口,如下所示:

interface IUnknown {
  virtual HRESULT QueryInterface (REFIID riid, void **ppvObject) = 0;
  virtual ULONG   AddRef () = 0;
  virtual ULONG   Release () = 0;
};

这些方法留给界面用户来实现。它们对于创建和销毁必须继承 IUnknown 的某些对象至关重要。在这种情况下,运行时知道这三个方法,并期望在调用它们时实现它们。因此,从某种意义上说,它们充当了对象本身与使用该对象的任何事物之间的契约。


the run-time is aware of the three methods and expects them to be implemented 由于它们是纯虚拟的,因此无法创建 IUnknown 的实例,因此所有子类必须实现所有此类方法才能仅进行编译。不实施它们并仅在运行时发现它们没有危险(但显然,人们可能错误地实施它们,当然!)。哇,今天我学习了 Windows #define一个带有单词 interface 的宏,大概是因为他们的用户不能只 (A) 看到名称中的前缀 I 或 (B) 查看类来查看它一个界面。啊
u
user3371350

这是一个完整的例子,说明了为什么使用虚拟方法。

#include <iostream>

using namespace std;

class Basic
{
    public:
    virtual void Test1()
    {
        cout << "Test1 from Basic." << endl;
    }
    virtual ~Basic(){};
};
class VariantA : public Basic
{
    public:
    void Test1()
    {
        cout << "Test1 from VariantA." << endl;
    }
};
class VariantB : public Basic
{
    public:
    void Test1()
    {
        cout << "Test1 from VariantB." << endl;
    }
};

int main()
{
    Basic *object;
    VariantA *vobjectA = new VariantA();
    VariantB *vobjectB = new VariantB();

    object=(Basic *) vobjectA;
    object->Test1();

    object=(Basic *) vobjectB;
    object->Test1();

    delete vobjectA;
    delete vobjectB;
    return 0;
}

e
edwinc

我认为您指的是一旦方法被声明为虚拟的事实,您就不需要在覆盖中使用“虚拟”关键字。

class Base { virtual void foo(); };

class Derived : Base 
{ 
  void foo(); // this is overriding Base::foo
};

如果您不在 Base 的 foo 声明中使用“virtual”,那么 Derived 的 foo 只会遮蔽它。


L
Leon Chang

这是前两个答案的 C++ 代码的合并版本。

#include        <iostream>
#include        <string>

using   namespace       std;

class   Animal
{
        public:
#ifdef  VIRTUAL
                virtual string  says()  {       return  "??";   }
#else
                string  says()  {       return  "??";   }
#endif
};

class   Dog:    public Animal
{
        public:
                string  says()  {       return  "woof"; }
};

string  func(Animal *a)
{
        return  a->says();
}

int     main()
{
        Animal  *a = new Animal();
        Dog     *d = new Dog();
        Animal  *ad = d;

        cout << "Animal a says\t\t" << a->says() << endl;
        cout << "Dog d says\t\t" << d->says() << endl;
        cout << "Animal dog ad says\t" << ad->says() << endl;

        cout << "func(a) :\t\t" <<      func(a) <<      endl;
        cout << "func(d) :\t\t" <<      func(d) <<      endl;
        cout << "func(ad):\t\t" <<      func(ad)<<      endl;
}

两个不同的结果是:

如果没有#define virtual,它会在编译时绑定。 Animal *ad 和 func(Animal *) 都指向 Animal 的 say() 方法。

$ g++ virtual.cpp -o virtual
$ ./virtual 
Animal a says       ??
Dog d says      woof
Animal dog ad says  ??
func(a) :       ??
func(d) :       ??
func(ad):       ??

使用#define virtual,它在运行时绑定。 Dog *d、Animal *ad 和 func(Animal *) 指向/引用 Dog 的 says() 方法,因为 Dog 是它们的对象类型。除非[Dog's say() "woof"] 方法没有定义,否则它将是在类树中最先搜索的方法,即派生类可以覆盖其基类[Animal's say()] 的方法。

$ g++ virtual.cpp -D VIRTUAL -o virtual
$ ./virtual 
Animal a says       ??
Dog d says      woof
Animal dog ad says  woof
func(a) :       ??
func(d) :       woof
func(ad):       woof

有趣的是,Python are effectively virtual 中的所有类属性(数据和方法)。由于所有对象都是在运行时动态创建的,因此不需要类型声明或关键字 virtual。下面是 Python 的代码版本:

class   Animal:
        def     says(self):
                return  "??"

class   Dog(Animal):
        def     says(self):
                return  "woof"

def     func(a):
        return  a.says()

if      __name__ == "__main__":

        a = Animal()
        d = Dog()
        ad = d  #       dynamic typing by assignment

        print("Animal a says\t\t{}".format(a.says()))
        print("Dog d says\t\t{}".format(d.says()))
        print("Animal dog ad says\t{}".format(ad.says()))

        print("func(a) :\t\t{}".format(func(a)))
        print("func(d) :\t\t{}".format(func(d)))
        print("func(ad):\t\t{}".format(func(ad)))

输出是:

Animal a says       ??
Dog d says      woof
Animal dog ad says  woof
func(a) :       ??
func(d) :       woof
func(ad):       woof

这与 C++ 的虚拟定义相同。请注意,d 和 ad 是两个不同的指针变量,它们引用/指向同一个 Dog 实例。表达式 (ad is d) 返回 True 并且它们的值是相同的


u
user2445507

你熟悉函数指针吗?虚函数是一个类似的想法,除了您可以轻松地将数据绑定到虚函数(作为类成员)。将数据绑定到函数指针并不容易。对我来说,这是主要的概念区别。这里的许多其他答案只是说“因为……多态性!”


r
rashedcs

我们需要支持“运行时多态性”的虚拟方法。当您使用指针或对基类的引用来引用派生类对象时,您可以为该对象调用虚函数并执行派生类的函数版本。


f
fishermanhat

底线是虚拟功能让生活更轻松。让我们使用 M Perry 的一些想法并描述如果我们没有虚函数而只能使用成员函数指针会发生什么。在没有虚函数的正常估计中,我们有:

 class base {
 public:
 void helloWorld() { std::cout << "Hello World!"; }
  };

 class derived: public base {
 public:
 void helloWorld() { std::cout << "Greetings World!"; }
 };

 int main () {
      base hwOne;
      derived hwTwo = new derived();
      base->helloWorld(); //prints "Hello World!"
      derived->helloWorld(); //prints "Hello World!"

好的,这就是我们所知道的。现在让我们尝试使用成员函数指针来实现:

 #include <iostream>
 using namespace std;

 class base {
 public:
 void helloWorld() { std::cout << "Hello World!"; }
 };

 class derived : public base {
 public:
 void displayHWDerived(void(derived::*hwbase)()) { (this->*hwbase)(); }
 void(derived::*hwBase)();
 void helloWorld() { std::cout << "Greetings World!"; }
 };

 int main()
 {
 base* b = new base(); //Create base object
 b->helloWorld(); // Hello World!
 void(derived::*hwBase)() = &derived::helloWorld; //create derived member 
 function pointer to base function
 derived* d = new derived(); //Create derived object. 
 d->displayHWDerived(hwBase); //Greetings World!

 char ch;
 cin >> ch;
 }

虽然我们可以用成员函数指针做一些事情,但它们不像虚函数那样灵活。在类中使用成员函数指针很棘手;成员函数指针几乎,至少在我的实践中,总是必须在主函数中或从成员函数中调用,如上例所示。

另一方面,虚函数虽然可能有一些函数指针开销,但确实大大简化了事情。

编辑:eddietree 还有另一种类似的方法: c++ virtual function vs member function pointer (performance comparison)


A
A. Hendry

跟进@user6359267 的回答,C++ 范围层次结构是

global -> namespace -> class -> local -> statement

因此,每个类都定义了一个范围。如果不是这种情况,子类中的重写函数实际上将重新定义同一范围内的函数,这是链接器不允许的:

在每个翻译单元中使用之前必须声明一个函数,并且一个函数只能在整个程序的给定范围内定义一次(跨所有翻译单元)

由于每个类都定义了自己的范围,因此被调用的函数是在调用该函数的对象的类中定义的函数。所以,

#include <iostream>
#include <string>

class Parent
{
public:
    std::string GetName() { return "Parent"; }
};

class Child : public Parent
{
public:
    std:::string GetName() { return "Child"; }
};

int main()
{
    Parent* parent = new Parent();
    std::cout << parent->GetName() << std::endl;

    Child* child = new Child();
    std::cout << child->GetName() << std::endl;

    *parent = child;
    std::cout << child->GetName() << std::endl;

    return 0;
}

输出

Parent
Child
Parent

因此,我们需要一种方法来告诉编译器要调用的函数应该在运行时而不是编译时确定。这就是 virtual 关键字的作用。

这就是为什么函数重载被称为编译时多态性(或早期绑定)而虚函数覆盖被称为运行时多态性(或后期绑定)的原因。

细节:

在内部,当编译器看到一个虚函数时,它会创建一个类成员指针,该指针通常使用 .*->* 运算符指向类的成员(而不是对象中该成员的特定实例) .他们的工作是允许您访问给定指向该成员的指针的类的成员。这些很少被程序员直接使用(也许除非你正在编写一个编译器来实现“虚拟”)。