我知道派生类可以简单地“重新定义”基类成员 函数,以及派生类对象的函数时 调用时,使用派生类中定义的函数,但...... 这不会使“虚拟”关键字变得多余吗?我读过了 这两种情况之间存在明显的显着差异(即:如果 你有一个指向派生类的基类指针,你调用 一个函数,如果它是虚拟的派生类函数将是 调用,但如果没有,将调用基类函数。
换句话说,能够重新定义会员的目的是什么 作为非虚函数,这是一个常用的 练?
就我个人而言,在我看来它会变得非常混乱。
谢谢!
答案 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 虚拟方法。例如,一个代表一个点的类,有两个成员x
和y
- 你可能有一个非常大的这些点的集合,一个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的“覆盖”关键字比“虚拟”更有用。