【问题标题】:Difference between calling of virtual function and non virtual function?调用虚函数和非虚函数的区别?
【发布时间】:2012-01-08 09:18:51
【问题描述】:

这其实是一道面试题,我想不出答案。有人知道吗? 您可以谈论任何差异,例如,压入堆栈的数据。

【问题讨论】:

  • 你的问题到底是什么?
  • 一个虚函数进行动态调度。您应该选择a good C++ book 以更好地学习它。
  • 我觉得这个问题有点含糊。这意味着什么不同?代码语义级别、编译器级别还是机器级别的区别?

标签: c++ virtual-functions function-calls


【解决方案1】:

虽然虚拟/动态调度是严格实现定义的,但大多数(阅读所有已知)编译器通过使用vptr 和vtable 来实现它。

话虽如此,调用非虚函数和虚函数的区别是:

非虚函数被解析为staticallyCompile-time,而虚函数被解析为dynamicallyRun-time。

为了实现这种能够决定在运行时调用哪个函数的灵活性, 在虚拟功能的情况下会有一点开销。

需要执行额外的fetch 调用,这是您为使用动态调度支付的开销/价格。

如果是非虚函数,调用顺序是:

fetch-call

编译器需要fetch函数的地址,然后call它。

而在虚函数的情况下,顺序是:

fetch-fetch-call

编译器需要从this到fetchvptr,然后从vptr到fetch函数的地址,再到call函数。

这只是一个简化的解释,实际的序列可能比这复杂得多,但这是你真正需要知道的,你不需要知道实现细节。

好读:

Inheritance & Virtual Functions

【讨论】:

  • 你能提供更多关于fetch-call和fetch-fetch-call过程的细节吗?或者你能提供一些读数吗?谢谢。
  • @cheng:添加了一个链接,很好地解释了细节。
  • 可能值得考虑额外的间接级别对缓存和流水线以及内存局部性的影响。纸上看起来简单的东西在实践中可能会产生不可忽视的影响。
  • 非虚函数的序列是call。函数指针的序列是fetch-call。一个 vtable 添加另一个 fetch。
  • 嗨,Alok,对答案的一项改进是,编译器从对象中获取 vptr 而不是这个。 this 在调用成员函数后出现
【解决方案2】:

如果您有一个基类“Base”和派生类“Derived”,并且您有一个在基类中定义为虚拟的函数“func()”。该函数被 Derived 类覆盖。

假设你定义

       Base obj = new Derived();
       obj.func();

然后调用 Derived 类的 'func'。虽然如果'func()' 没有在 Base 中定义为虚拟,那么它将从 'Base' 类中调用。这就是虚拟函数和非虚拟函数的函数调用不同的区别

【讨论】:

  • 一些微妙的点: 1. obj 需要是一个指针。 2.容易提到覆盖函数Derived::func()应该采用相同的参数来覆盖Base::func()
  • 这不是一个微妙的点,而是一个至关重要的点。 “obj.func()”不会调用基类方法,但必须使用指针并调用“obj->func()”才能使用虚拟方法。
  • 请注意,可以使用引用而不是指针,尽管并非总是在相同的情况下。例如,引用不能被重新绑定以引用不同的对象。
【解决方案3】:

非虚拟成员函数是静态解析的。成员函数在编译时静态绑定,基于指向对象的指针(或引用)的类型。

相比之下,虚拟成员函数是在运行时动态绑定。如果类至少有一个虚成员函数,那么编译器会在对象的构造过程中在对象中放置一个名为 vptr(virtual table address) 的隐藏指针。

编译器为每个类创建一个 v-table,其中至少有一个虚函数。虚表包含虚函数的地址。它可以是虚函数指针的数组或列表(取决于编译器)
在一个虚函数的调度过程中,运行时系统跟随对象的 v-pointer(从类对象获取地址)到类的 v-table,然后将偏移量添加到基地址(vptr)并调用该函数。

上述技术的 空间成本 开销是名义上的:每个对象有一个额外的指针(但仅适用于需要进行动态绑定的对象),以及每个方法的额外指针(但仅限对于虚拟方法)。 时间成本开销也相当小:与普通函数调用相比,虚函数调用需要两次额外的提取(一次获取 v 指针的值,第二次获取地址的方法)。

这种运行时活动不会发生在非虚拟函数上,因为编译器会在编译时根据指针的类型专门解析非虚拟函数。

我举了一个简单的例子来更好地理解非虚函数和虚函数的绑定是如何发生的,以及虚函数机制是如何工作的。

#include<iostream>
using namespace std;
class Base
{
        public:
                virtual void fun()
                {}
                virtual void fun1()
                {}

                void get()
                {
                        cout<<"Base::get"<<endl;
                }
                void get1()
                {
                        cout<<"Base::get1"<<endl;
                }
};

class Derived :public Base
{
        public:
                void fun()
                {
                }
                virtual void fun3(){}
                void get()
                {
                        cout<<"Derived::get"<<endl;
                }
                void get1()
                {
                        cout<<"Derived::get1"<<endl;
                }

};
int main()
{
    Base *obj = new Derived();
    obj->fun();
    obj->get();
}

vtable 如何为基类和派生类创建

会生成汇编代码以便更好地理解。

$ g++ virtual.cpp -S -o virtual.s

我已经分别从 virtual.s 中获取了 Base 和 Derived 类的 vtable 信息:

_ZTV4Base:
        .quad   _ZN4Base3funEv
        .quad   _ZN4Base4fun1Ev
_ZTV7Derived:
        .quad   _ZN7Derived3funEv
        .quad   _ZN4Base4fun1Ev
        .quad   _ZN7Derived4fun3Ev

如您所见,fun 和 fun1 只是 Base 类中的两个虚函数。 Base class(_ZTV4Base)的Vtable有两个虚函数的入口。 Vtable 没有非虚函数的入口。请不要与 fun(ZN4Base3funEv) 和 fun1(ZN4Base4fun1Ev) 的名称混淆,它们的名称被混淆了。

派生类vtable有树条目

  1. fun(_ZN7Derived3funEv) 覆盖函数
  2. fun1(_ZN4Base4fun1Ev) 继承自基类
  3. fun3(_ZN7Derived4fun3Ev) 派生类中的新函数

非虚函数和虚函数如何调用?

非虚函数

    Derived d1;
    d1.get();

    subq    $16, %rsp
    leaq    -16(%rbp), %rax
    movq    %rax, %rdi
    call    _ZN7DerivedC1Ev //call constructor
    leaq    -16(%rbp), %rax
    movq    %rax, %rdi
    call    _ZN7Derived3getEv //call get function

简单地告诉、获取和调用get(绑定发生在编译时)

非虚函数

Base *obj = new Derived();
obj->fun();
pushq   %rbx
subq    $24, %rsp
movl    $8, %edi
call    _Znwm   //call new to allocate memory 
movq    %rax, %rbx
movq    $0, (%rbx)
movq    %rbx, %rdi
call    _ZN7DerivedC1Ev //call constructor
movq    %rbx, -24(%rbp)
movq    -24(%rbp), %rax
movq    (%rax), %rax
movq    (%rax), %rax
movq    -24(%rbp), %rdx
movq    %rdx, %rdi
call    *%rax //call fun

获取vptr,添加函数偏移,调用函数(绑定发生在运行时)

64 的汇编让大多数 c++ 程序员感到困惑,但如果有任何想讨论的,欢迎

【讨论】:

  • 在动态链接模块中调用普通函数可能涉及指针间接。
【解决方案4】:

调用虚方法时,必须在虚函数表中查找要调用的函数。

【讨论】:

  • 那么调用虚函数和非虚函数有什么区别呢?例如,他们每个人需要多少内存访问?
  • @cheng:我不知道虚拟方法调用比非虚拟方法调用有多少访问(我假设一两个),但虚拟方法调用确实需要更多工作。
  • 好的,我会尝试解决这个问题。谢谢。
  • 接受的答案提供了有关此问题的良好阅读。见上文。
【解决方案5】:

调用虚方法的开销很大。

Also this.

【讨论】:

    猜你喜欢
    • 2012-02-06
    • 2015-09-17
    • 1970-01-01
    • 2013-11-13
    • 2011-04-16
    • 2021-06-01
    • 2021-07-07
    • 2011-06-16
    • 2019-04-14
    相关资源
    最近更新 更多