C++中的`virtual`关键字难以理解

5
我在理解C++中virtual关键字的作用方面遇到了困难。我非常熟悉C和Java,但是对C++还不太了解。
从维基百科上可以得知:
在面向对象编程中,虚函数或虚方法是一种函数或方法,其行为可以被一个具有相同签名的函数所覆盖(override)。
然而,我可以像下面这样重写一个方法,而不需要使用virtual关键字。
#include <iostream>

using namespace std;

class A {
    public:
        int a();
};

int A::a() {
    return 1;   
}

class B : A { 
    public:
        int a();
};

int B::a() {
    return 2;
}

int main() {
    B b;
    cout << b.a() << endl;
    return 0;
}

//output: 2

正如您在下面看到的那样,函数A::a被成功地覆盖为B::a,而不需要使用virtual关键字。
让我更加困惑的是维基百科上有关虚析构函数的这个声明:
正如以下示例所示,C++基类必须具有虚析构函数,以确保始终调用最派生类的析构函数。
所以,virtual还告诉编译器调用父类的析构函数吗?这似乎与我最初对virtual的理解“使函数可重写”的理解非常不同。
9个回答

16

按照以下更改,你会明白原因:

#include <iostream>

using namespace std;

class A {
    public:
        int a();
};

int A::a() {
    return 1;   
}

class B : public A { // Notice public added here
    public:
        int a();
};

int B::a() {
    return 2;
}

int main() {
    A* b = new B(); // Notice we are using a base class pointer here
    cout << b->a() << endl; // This will print 1 instead of 2
    delete b; // Added delete to free b
    return 0;
}
现在,为了让它按照你的意图工作:
#include <iostream>

using namespace std;

class A {
    public:
        virtual int a(); // Notice virtual added here
};

int A::a() {
    return 1;   
}

class B : public A { // Notice public added here
    public:
        virtual int a(); // Notice virtual added here, but not necessary in C++
};

int B::a() {
    return 2;
}

int main() {
    A* b = new B(); // Notice we are using a base class pointer here
    cout << b->a() << endl; // This will print 2 as intended
    delete b; // Added delete to free b
    return 0;
}
你所提到的虚析构函数的注释完全正确。在你的示例中,没有需要清除的内容,但是假设 A 和 B 都有析构函数。如果它们没有标记为虚拟的话,使用基类指针调用哪个析构函数?提示: 当未标记为虚拟函数时,它的行为将与 a() 方法完全相同。

2
不需要用new/delete来复杂化示例:B obj; A* p = &obj; 或者 A& ref = obj; - Roger Pate
1
真的 - 我在第一次编辑中实际上忘记了删除操作; 但是C++中的解引用语义也很复杂...;-) - ConsultUtah

9
你可以这样理解:Java 中的所有函数都是虚函数。如果你有一个带有函数的类,并且在派生类中重写了该函数,无论你用于调用它的变量的声明类型是什么,它都将被调用。
然而,在 C++ 中,它不一定会被调用。如果你有一个基类 Base 和一个派生类 Derived,并且它们都有一个名为“foo”的非虚函数,则...
Base * base;
Derived *derived;

base->foo(); // calls Base::foo
derived->foo(); // calls Derived::foo

如果foo是虚函数,则都调用Derived :: foo。

2
所以virtual也告诉编译器调用父类的析构函数?这似乎与我最初对virtual的理解“使函数可以重载”非常不同。
你最初的和现在的理解都是错误的。
方法(你称之为函数)始终可重载。无论是虚的、纯的、非虚的还是其他类型的方法。
父类的析构函数总是被调用的。构造函数也是如此。
“虚”的唯一区别是,如果你通过指向基类的指针类型调用一个方法。由于在你的示例中根本没有使用指针,所以虚根本没有任何区别。
如果你使用类型为pointer-to-A的变量a,即A* a;不仅可以将其他类型为pointer-to-A的变量赋值给它,还可以将类型为pointer-to-B的变量赋值给它,因为B是从A派生的。
A* a; 
B* b;

b = new B(); // create a object of type B. 
a = b;       // this is valid code. a has still the type pointer-to-A, 
             // but the value it holds is b, a pointer to a B object.

a.a();       // now here is the difference. If a() is non-virtual, A::a()
             // will be called, because a is of type pointer-to-A. 
             // Whether the object it points to is of type A, B or
             // something entirely different doesn't matter, what gets called
             // is determined during compile time from the type of a.

a.a();       // now if a() is virtual, B::a() will be called, the compiler
             // looks during runtime at the value of a, sees that it points
             // to a B object and uses B::a(). What gets called is determined
             // from the type of the __value__ of a.

+1,你可能想要添加一个声明,即函数始终是可重写的。在C++0x中,关键字“final”应该可以防止覆盖。 - Tim

2

虚拟的意思是实际方法是根据实例化的类来确定运行时,而不是你用来声明变量的类型。 在您的情况下,这是一个静态重写,它将使用为类B定义的方法,无论创建的对象的实际类型是什么


1
正如您在下面所看到的,函数 A::a 被成功地重写为 B::a,而无需使用 virtual。
这可能有效,也可能无效。在你的例子中它有效,但这是因为你直接创建并使用了一个 B 对象,而不是通过指向 A 的指针。参见 C++ FAQ Lite, 20.3
虚拟析构函数之所以需要,是因为如果你删除一个指向派生类对象的基类指针,并期望运行基类和派生类析构函数,则需要使用虚拟析构函数。参见 C++ FAQ Lite, 20.7

1

如果您使用基类指针,如consultutah(以及我打字时的其他人;))所说,您需要虚拟函数。

缺少虚拟函数可以节省检查以了解它需要调用哪个方法(基类或某些派生类的方法)。但是,在这一点上不要担心性能,只关注正确的行为。

虚拟析构函数特别重要,因为派生类可能在堆上声明其他变量(即使用关键字“new”),您需要能够删除它。

然而,您可能会注意到,在C ++中,您倾向于使用比Java少的派生(通常使用模板进行类似的用途),甚至可能根本不需要担心。此外,如果您从未在堆上声明对象(“A a;”而不是“A * a = new A();”),则也不需要担心。当然,这将严重取决于您开发的内容/方式以及是否计划有其他人派生您的类。


1
重点不在于对象分配的位置,而在于通过指针和基类引用来使用它们,包括方法中隐含的“this”。 - Roger Pate

0

假设你实例化了 B,但将其作为 A 的实例持有:

A *a = new B();

如果调用函数a(),那么将调用a()的实现?

如果a()不是虚拟的,那么将调用A的a()。如果a()是虚拟的,则不管你如何保留它,都将调用实例化的子类版本的a()。

如果B的构造函数为数组分配了大量内存或打开文件,则调用

delete a;

无论通过基类、接口或其他方式持有B对象,都会确保调用B的析构函数。

顺便说一句,这是个好问题。


哎呀,谢谢。你猜谁已经连续三年只用VB.net和C#了? - Mark Holland

0

我总是把它想象成棋子(我的第一个面向对象编程实验)。

棋盘保存了所有棋子的指针。空格是NULL指针。但它只知道每个指针指向一个棋子。棋盘不需要知道更多信息。但当一个棋子被移动时,棋盘并不知道这是一个有效的移动,因为每个棋子都有不同的移动特性。所以棋盘需要与棋子检查移动是否有效。

Piece*    board[8][8];

CheckMove(Point const& from,Point const& too)
{
    Piece*  piece = board[from.x][from.y];
    if (piece != NULL)
    {
        if (!piece->checkValidMove(from,too))
        {    throw std::exception("Bad Move");
        }
        // Other checks.
    }
}

class Piece
{
    virtual bool checkValidMove(Point const& from,Point const& too)  = 0;
};

class Queen: public Piece
{
    virtual bool checkValidMove(Point const& from,Point const& too) 
    {
         if (CheckHorizontalMove(from,too) || CheckVerticalMoce(from,too) || CheckDiagonalMove(from,too))
         {
             .....
         }
    }
}

0

尝试使用((A*)&b).a()并查看调用了什么。

虚拟关键字允许您以抽象方式处理对象(即通过基类指针),但仍然调用后代代码...

换句话说,虚拟关键字“让旧代码调用新代码”。您可能已经编写了操作A的代码,但是通过虚拟函数,该代码可以调用B的新a()。


网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接