想象一下,您有以下课程:
class A {
public:
virtual void print() { printf("A\n"); }
};
class B : public A {
public:
virtual void print() override { printf("B\n"); }
};
class C : public B {
// no override of print
};
现在,如果您创建B的实例并调用print:
B * b = new B;
b->print();
这种方法会被虚拟调用吗?换句话说,在编译时或运行时确定要调用的确切方法吗?
理论上它可以在编译时确定,因为我们知道,B的子类都不会覆盖该方法,因此无论我分配到指向B B * b = new C; b->print();
的指针,它总是会调用{ {1}}。
编译器是否也知道它并使我免于虚拟调用的不必要开销?
答案 0 :(得分:3)
从理论上讲,它可以在编译时确定,因为我们知道,B的子类都不会覆盖该方法
在一般情况下,您无法在编译时确定这一点,因为C ++编译器一次只处理一个转换单元。来自不同翻译单元的类,例如class D : public B
可以覆盖该方法。但是,在转换class D
的调用时,编译器可能无法查看b->print()
的转换单元,因此编译器必须采用虚拟调用。
为了解决这个缺点,C ++ 11引入了final
keyword,它允许程序员告诉编译器,从这个级别的继承层次结构中不会有进一步的覆盖。现在,编译器可以优化虚拟调用,并强制执行不再进行覆盖的要求。
答案 1 :(得分:2)
关于
“理论上它可以在编译时确定,因为我们知道,B的子类都没有覆盖那个方法,所以无论我指定什么指向BB * b =新C; b-> print();,它将始终调用B :: print()。
是强>
然而,编译器是否会进行此优化完全取决于编译器,它知道什么,以及你告诉它做什么。
编译器知道什么取决于许多因素,例如
是否在多个翻译单元中定义了单独编译的类?
您使用的是全局优化吗?
您是否可以使用关键字final
来通知编译器?
根据您的具体示例,
B * b = new B;
b->print();
其中print
是虚拟的,无论编译器如何,我都会非常自信地称它为非虚拟的,因为编译器知道 b
所指的是什么。我们来看看。
好的,使用MinGW g ++ 5.1和选项-O2
(我没有尝试其他任何操作),呼叫被编译为直接调用puts
,甚至绕过printf
。
答案 2 :(得分:1)
标准除外,现代编译器还具有DLL和共享库等编译功能,并处理原始共享内存的情况,其他进程new
编写了一些对象实例。
所以看到" Common"并不常见。或"共享"保存某个类的接口的文件夹,两个项目包括该标头并从该接口派生。
所以在你的例子中,让我们说整个声明都在一个头文件中,其他一些项目可能包含那个头,并派生自B
这是如何从DLL导出类指针并从共享库调用正确的函数。
正如我在评论中所写: 虚函数有一个开销,但无论如何还要2~3个装配线。你将v表加载到某个寄存器中,将该寄存器递增一些偏移量就完成了。为了看到差异的一秒,你将不得不调用毫秒次的虚拟功能。 usuall瓶颈是非友好的缓存变量,内存分配,无论如何都是CPU和IO,而不是虚函数