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

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

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

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


当前回答

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

虚拟析构函数

考虑下面的这个程序,不要将基类析构函数声明为virtual;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;
}

输出:

删除动物名称猫删除动物

其他回答

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

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

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

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


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

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

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

快速回答:

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

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

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

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

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


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

2.您不能在运行时使用其他语言功能在备选方案中进行选择,从而使功能更快或使用更少的内存。Bjarne Stroustrup C++编程:原理与实践。(14.3.1).

3.当我们调用包含虚拟函数的基类时,可以判断哪个函数真正被调用。

底线是,虚拟功能使生活更轻松。让我们使用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;
 }

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

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

EDIT:还有一种方法与eddietree类似:c++虚拟函数与成员函数指针(性能比较)。

当基类中有函数时,可以在派生类中重新定义或重写它。

重新定义方法:派生类中给出了基类方法的新实现。不便于动态绑定。

重写方法:在派生类中重新定义基类的虚拟方法。虚拟方法有助于动态绑定。

所以当你说:

但在书的早些时候,当我了解基本遗传时能够重写派生类中的基方法,而不使用“虚拟”。

因为基类中的方法不是虚拟的,所以您不是在重写它,而是在重新定义它