虚拟和非虚拟成员函数的调用方式有什么区别?


#include<iostream>
using namespace std;
int main(){
class c1{
public:
int func(){
cout<<"in the  c1";
}
};
class c2:public c1{
public:
int func(){
cout<<" in c2";
}
};

c1* a;
c2 b;
a=&b;
a->func();
}

我知道我应该使用虚拟函数来获得所需的结果,但我想知道上面的代码中发生了什么。例如,为什么要调用c1::func()而不是c2::fun()?

此外,请解释使用virtual时会发生什么,这与本例不同。

当成员函数不是virtual时,调用的函数仅由点(.)或箭头(->)运算符左侧的表达式类型决定。这被称为"静态类型"。

当成员函数为virtual时,调用的函数由对象的实际最派生类型确定,该对象由点左侧的表达式命名(.)或由箭头左侧的表达式指向(->)。这被称为"动态类型"。

请注意,当点左侧使用的变量、成员、参数或返回类型具有普通类类型时,静态类型和动态类型始终相同。但是,如果变量、成员、参数或返回类型是对类类型的指针或引用,则静态类型和动态类型可能不同。

请参阅http://www.cs.technion.ac.il/users/yechiel/c++-faq/dyn-binding.html,我引用:

非虚拟成员函数是静态解析的。也就是说,成员函数是根据对象的指针(或引用)类型静态选择的(在编译时)。

相反,虚拟成员函数是动态解析的(在运行时)。也就是说,成员函数是根据对象的类型而不是该对象的指针/引用的类型动态选择的(在运行时)。这被称为";动态绑定"大多数编译器使用以下技术的一些变体:如果对象有一个或多个虚拟函数,编译器会在名为"的对象中放置一个隐藏指针;虚拟指针";或";v指针"这个v指针指向一个称为";虚拟表";或";v-table">

查看以C风格实现的虚拟和非虚拟函数可能会有所帮助。

struct bob {
int x;
static int get_x_1( bob* self ){ return self->x; }
int get_x_2() { return this->x; }
};
inline int get_x_3( bob* self ){ return self->x; }

以上三个基本上都是一样的(调用约定之类的小细节——寄存器或堆栈位置参数的执行方式——可能会有所不同)。

非虚拟成员函数只是使用名为this的半秘密指针的函数。半机密,因为它就在方法名称的左边。

理解这一点很重要。非虚拟调用只是一些奇特的函数调用。类的实例不存储指向其方法或类似方法的指针。它们只是单调函数调用的语法糖。


现在虚拟成员的功能有所不同。

以下是我们将如何实现的大致方法:

class bob{
public:
virtual int get_x(){ return x; }
int x;
};

不使用virtual:

struct bob;
using get_x=int(bob*);
struct bob_vtable {
get_x* get_x=0;
};
inline int get_x_impl( bob* self );
bob_vtable* get_bob_vtable(){
static bob_vtable vtable{ get_x_impl };
return &vtable;
}
struct bob {
bob_vtable* vtable=0;
int x;
int get_x(){ return this->vtable->get_x(this); }
bob(): vtable(get_bob_vtable()) {}
};
inline int get_x_impl( bob* self ){ return self->x; }

发生了很多事情。

首先我们有get_x_impl,它非常像上面的非虚拟get_xs。

Sexand,我们有一个函数指针表(这里只包含一个),称为vtable。我们的bob包含一个指向vtable的指针作为它的第一个条目。在它中,我们有一个指向get_x_impl的函数指针。最后,bob有一个get_x方法,它通过vtable、函数指针将调用转发到get_x_impl

在构造过程中,派生类型可以通过get_x的不同实现将vtable指针更改为指向不同的函数表。

然后,当ypu有一个指向bob的指针并调用get_x时,它将跟随更改后的vtable中的指针,并调用替换实现。

当你创建一个虚拟函数时,像上面这样的机器就是为你编写的。当您继承和重写时,用派生的替换父vtable指针的代码将被注入到派生类型的构造函数中。

所有这些基本上都实现了在C++存在之前,C语言中的人可以做的事情。他们只是隐藏了细节,并用C++为您编写了粘合代码。

最新更新