13

这实际上是一个面试问题,我无法弄清楚答案。有人知道吗?您可以谈论任何差异,例如,推入堆栈的数据。

4

5 回答 5

30

尽管虚拟/动态调度是严格实现定义的,但大多数(阅读所有已知的)编译器使用vptrand来实现它vtable

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

非虚函数在statically处解析Compile-time,而虚函数在dynamically处解析Run-time

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

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

在非虚函数的情况下,调用顺序是:

fetch-call

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

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

fetch-fetch-call

编译器需要fetch从,然后vptr从函数的地址thisfetchvptr然后call是函数。

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

好读:

继承和虚函数

于 2012-01-08T09:25:27.040 回答
7

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

假设你定义

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

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

于 2012-01-08T09:29:38.543 回答
4

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

相反,虚成员函数在运行时动态绑定的。如果类至少有一个虚拟成员函数,那么编译器会在对象构造过程中在对象中放置一个隐藏指针,称为vptr(虚拟表地址)

编译器为每个具有至少一个虚函数的类创建一个 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++ 程序员感到困惑,但如果有任何想讨论的,欢迎

于 2015-12-25T12:14:04.023 回答
3

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

于 2012-01-08T09:21:21.010 回答
3

调用虚方法的开销很大。

还有这个。

于 2012-01-08T09:22:02.717 回答