C ++多态/继承问题:基本函数与虚函数的重新定义

时间:2011-02-03 02:26:41

标签: c++ inheritance virtual-functions

我知道派生类可以简单地“重新定义”基类成员 函数,以及派生类对象的函数时 调用时,使用派生类中定义的函数,但...... 这不会使“虚拟”关键字变得多余吗?我读过了 这两种情况之间存在明显的显着差异(即:如果 你有一个指向派生类的基类指针,你调用 一个函数,如果它是虚拟的派生类函数将是 调用,但如果没有,将调用基类函数。

换句话说,能够重新定义会员的目的是什么 作为非虚函数,这是一个常用的 练?

就我个人而言,在我看来它会变得非常混乱。

谢谢!

8 个答案:

答案 0 :(得分:7)

最常见的OOP语言(Java,SmallTalk,Python等)最常见的方法是默认情况下每个成员函数都为virtual

缺点是每次进行虚拟呼叫时都会有很小的性能损失。因此,C ++允许您选择是否要将方法定义为虚拟方法。

但是,虚拟方法和非虚方法之间存在非常重要的差异。例如:

class SomeClass { ... };
class SomeSubclassOfSomeClass : public SomeClass { ... };
class AnotherSubclassOfSomeClass : public SomeClass { ... };

SomeClass* p = ...;

p->someVirtualMethod();

p->someNonVirtualMethod();

调用someVirtualMethod时执行的实际代码取决于引用指针p的具体类型,完全取决于SomeClass子类重新定义。

但是someNonVirtualMethod调用上执行的代码很明确:始终是SomeClass上的代码,因为p变量的类型是SomeClass

答案 1 :(得分:2)

听起来你已经知道了虚拟和非虚拟方法之间的区别,所以我不会像其他方法那样进入。问题是,非虚拟方法何时比虚拟方法更有用?

在某些情况下,您不希望在每个对象中包含vtable指针的开销,因此您需要努力确保类中有 no 虚拟方法。例如,一个代表一个点的类,有两个成员xy - 你可能有一个非常大的这些点的集合,一个vtable指针会增加对象的大小at至少50%。

答案 2 :(得分:1)

它适用于派生类的实例和指向派生类的指针。但是,如果将派生类传递给带有指向Base的指针的函数,则将调用函数的Base版本。这可能是不可取的。例如,以下将返回5

#include "stdafx.h"
#include <conio.h>
#include <iostream>

class Base
{
public:
    int Foo(){return 5;}
};

class Derived:public Base
{
    int Foo(){return 6;}
};

int Func(Base* base)
{
    return base->Foo();
}

int _tmain(int argc, _TCHAR* argv[])
{
    Derived asdf;

    std::cout << Func(&asdf);
    getch();

    return 0;
}

这是因为虚拟的工作方式。当对象具有虚拟调用时,在调用虚函数时,在v表中查找正确的函数。否则你实际上没有继承,你的Base指针就像基类而不是派生类。

答案 3 :(得分:1)

解决:“换句话说,能够将成员函数重新定义为非虚函数的目的是什么,这是一种常用的做法吗?”

嗯,你不能。如果基类方法是虚拟的,那么对应的派生类方法也是如此,如果存在,则是否使用'virtual'关键字。

所以:“这不会使”虚拟“关键字变得多余吗?” 是的,派生类方法中的 是多余的,但基类中的不是

但是,请注意,希望拥有非虚方法然后将其重新定义为派生类是不寻常的(礼貌)。

答案 4 :(得分:0)

在派生类中将析构函数标记为非虚拟和“重写”时,请务必小心 - 如果在指向基类的指针上调用析构函数,则可能无法正确清理类。

答案 5 :(得分:0)

似乎你还没有发现多态性的力量。多态性以这种方式起作用:

你有一个函数,它接受一个基类的指针,你可以通过传入派生类对象来调用它,根据派生类的不同实现,该函数将采取不同的行为。这是一种很棒的方式,因为即使我们扩展了继承树的层次结构,函数也是稳定的。

没有这种机制,你无法做到这一点。而这种机制需要“虚拟”

答案 6 :(得分:0)

C ++作用域和名称查找规则允许非常奇怪的事情,并且这里的方法并不孤单。确实隐藏(或阴影)可能会在许多不同情况下发生:

int i = 3;
for (int i = 0; i != 5; ++i) { ... } // the `i` in `for` hides the `i` out of it

struct Base
{
  void foo();
  int member;
};

struct Derived: Base
{
  void foo(); // hides Base::foo
  int member; // hides Base::member
};

为什么呢?为了弹性。

修改Base课程时,我不知道所有可能的孩子。由于隐藏规则(尽管它可能会产生混淆),我可以添加一个属性或方法,并在世界范围内无需小心使用它:

  • 我的电话将始终调用Base方法,无论某个孩子是否覆盖
  • 对孩子的打电话不会受到影响,因此我不会突然崩溃其他程序员的代码

很明显,如果你把这个程序视为一项有限的工作它没有意义,但程序正在发展,隐藏规则可以简化进化。

答案 7 :(得分:0)

最简单的解释方法可能就是:

  

Virtual通过添加虚拟查找表为您进行一些查找。

换句话说,如果你没有虚拟关键字,并覆盖了一个方法,你仍然需要手动调用该方法[请原谅我,如果我的C ++语法内存有点生锈]:

class A { void doSomething() { cout << "1"; } }
class B: public A { void doSomething() { cout << "2"; } }
class C: public A { void doSomething() { cout << "3"; } }

void someOtherFunc(A* thing) {
    if (typeid(thing) == typeid(B)) {
        static_cast<B*>(thing)->doSomething();
    } else if (typeid(thing) == typeid(C)) {
        static_cast<C*>(thing)->doSomething();
    } else {
        // not a derived class -- just call A's method
        thing->doSomething();
    }
}

您可以使用查找表优化这一点(可读性和性能,最有可能):

typedef doSomethingWithAnA(A::*doSomethingPtr)();
map<type_info, doSomethingWithAnA> A_doSomethingVTable;

void someOtherFuncA* thing) {
   doSomethingWithAnA methodToCall = A_doSomethingVTable[typeid(thing)];
   thing->(*methodToCall)();
}

现在,这更像是一种高级方法。通过准确知道“type_info”是什么,C ++编译器显然可以更多地优化这一点,依此类推。所以可能,而不是“map”和查找“methodToCall = aDoSomethingVTable [typeid(thing)]”,然后调用,“,编译器插入更小更快的东西,如”doSomethingWithAnA * A_doSomethingVTable;“后跟”A_doSomethingTablething“ - &GT; TYPE_NUMBER”

所以你是对的,C ++并不真正需要虚拟,但它确实增加了很多语法糖,使你的生活更轻松,并且可以更好地优化它。

尽管如此,我仍然认为C ++是一种可怕的过时语言,有许多不必要的复杂性。例如,虚拟可以(并且可能应该)默认情况下被假设,并在不必要的地方进行优化。类似Scala的“覆盖”关键字比“虚拟”更有用。