我正在学习C++,我刚刚开始学习虚拟函数。

从我(在书中和网上)读到的内容来看,虚拟函数是基类中的函数,可以在派生类中重写。

但在本书的早些时候,当我学习基本继承时,我能够在派生类中重写基函数,而不使用虚函数。

那么我在这里错过了什么?我知道虚拟函数还有很多,它似乎很重要,所以我想清楚它到底是什么。我只是在网上找不到一个直截了当的答案。


当前回答

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

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

其他回答

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

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

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

您需要虚拟方法来实现安全的下变频、简单和简洁。

这就是虚拟方法所做的:它们安全地向下转换,使用明显简单而简洁的代码,避免了在更复杂和冗长的代码中进行不安全的手动转换。


Non-virtual method ⇒ static binding ========================================

以下代码故意“不正确”。它没有将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方法不是虚拟的。


Virtual method ⇒ dynamic binding. ======================================

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

#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。它们可以被标记为虚拟,但它们是自动虚拟的。override关键字确保如果某个基类中没有这样的虚拟方法,那么您将得到一个错误(这是可取的)。


The ugliness of doing this without virtual methods ==================================================

如果没有虚拟绑定,则必须实现一些自己动手版本的动态绑定。这通常涉及不安全的手动降级、复杂性和冗长。

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

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

看待这一点的一种积极方式是,如果您遇到了如上所述的不安全的下变频、复杂性和冗长,那么通常一个或多个虚拟方法确实会有帮助。

OOP答案:亚型多态性

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

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

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

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

https://en.wikipedia.org/wiki/Subtypinghttps://en.wikipedia.org/wiki/Polymorphism_(computer_science)#子类型

技术答案:动态调度

如果您有一个指向基类的指针,那么方法的调用(声明为虚拟)将被分派到所创建对象的实际类的方法。这就是亚型多态性是如何在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类图

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

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

如果你知道潜在的机制,这会有所帮助。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的虚拟方法。