C++虚函数的实现机制

  C++虚函数的实现机制

C++中的虚函数的作用主要是实现了多态的机制,多态,简而言之就是用父类型别的指针指向其子类的实例,然后通过父类的指针调用实际子类的成员函数。这种技术可以让父类的指针有“多种形态”,这是一种泛型技术。所谓泛型技术,就是试图使用不变的代码来实现可变的算法。比如:模板技术,RTTI技术,虚函数技术,要么是试图做到在编译时决议,要么试图做到运行时决议。

 

什么是虚函数?

简单地说,那些被virtual关键字修饰的成员函数,就是虚函数。 基类中声明为虚函数的成员方法,到达子类时仍然是虚函数,即使子类中重新定义基类虚函数时未使用virtual修饰。

虚函数的作用是什么?

    虚函数可以让成员函数操作具有一般化,用基类的指针指向不同的派生类的对象时,基类指针调用其虚成员函数,则会调用其真正指向对象的成员函数,而不是基类中定义的成员函数(只要派生类改写了该成员函数)。若不是虚函数,则不管基类指针指向的哪个派生类对象,调用时都会调用基类中定义的那个函数。

虚函数的实现机制?

虚函数(Virtual Function)是通过一张虚函数表(VirtualTable)和虚函数表指针(Virtual Table Pointer)来实现的。简称为vtbl和vptr。一个vtbl通常是一个函数指针数组(一些编译器使用链表来代替数组,但是基本方法都是一样的)在程序中的每个类只要声明(或继承)虚函数的类都有一个 vtbl ,而其中的每一项保存的是该 class 的各个虚函数实现体的指针。

class C1

{
  public:
    C1();
    virtual ~C1();
    virtual voidf1();

    virtual int f2(char c) const;

    virtual void f3(const string& s);

    void f4() const;

...

}

C1的Virtual table 看起来如下图:


 

注意非虚函数 f4 不在表中,而且C1的构造函数也不在。非虚函数(包括构造函数,
它也被定义为非虚函数)就像普通的 C 函数那样被实现。

如果有一个 C2 类继承自 C1,重新定义了它继承的一些虚函数,并加入了它自己的一些虚函数
class C2: public C1

{
   public:
     C2();                         // 非虚函数

    virtual ~C2();                //重定义函数

    virtual void f1();          //重定义函数

    virtual void f5(char *str); // 新的虚函数
     ...

}

它的 virtual table 每一项指向与对象相适合的函数。 这些项中包括指向没有被 C2 重定义的 C1 虚函数的指针:

 

基类的虚函数表的创建:

首先在基类声明中找到所有的虚函数,按照其声明顺序,编码0,1,2,3,4……,然后按照此声明顺序为基类创建一个虚函数表,其内容就是指向这些虚函数的函数指针,按照虚函数声明的顺序将这些虚函数的地址填入虚函数表中。

 

对于子类的虚函数表:

首先将基类的虚函数表复制到该子类的虚函数表中。若子类重写了基类的虚函数,则将子类的虚函数表中存放的函数地址(未重写前存放的是子类的虚函数的函数地址)更新为重写后函数的函数指针。若子类增加了一些虚函数的声明,则将这些虚函数的地址加到该类虚函数表的后面。

 

为了实现多态的机制,你必须为每个包含虚函数的类的 virtual talbe 留出空间。类的 vtbl 的大小与类中声明的虚函数的数量成正比(包括从基类继承的虚函数)。

对于virtualtable pointer,每个声明了虚函数类的对象都带有它,它是一个看不见的数据成员,指向对应类的virtualtable。这个看不见的数据成员也称为 vptr,被编译器加在对象里,位置只有才编译器知道。从理论上讲,我们可以认为包含有虚函数的对象的布局是这样的:


这幅图片表示vptr 位于对象的底部, 但是不要被它欺骗,不同的编译器放置它的位置也不同。存在继承的情况下,一个对象的vptr 经常被数据成员所包围。

假如我们有一个程序,包含几个 C1 和 C2 对象。 对象、vptr和刚才我们讲述的 vtbl之间的关系,在程序里我们可以这样去想象:


 

虚函数表是如何访问的?

考虑这段这段程序代码:
void makeACall(C1 *pC1)
{
   pC1->f1();
}

通过指针 pC1 调用虚拟函数 f1。仅仅看这段代码,你不会知道它调用的是那一个 f1函数――C1::f1 或 C2::f1,因为pC1 可以指向 C1 对象也可以指向 C2 对象。

编译器生成的代码会做如下这些事情:

通过对象的 vptr 找到类的 vtbl。 这是一个简单的操作, 因为编译器知道在对象内哪里能找到 vptr(毕竟是由编译器放置的它们)。 因此这个代价只是一个偏移调整(以得到vptr)和一个指针的间接寻址(以得到 vtbl)。

找到对应 vtbl 内的指向被调用函数的指针(在上例中是 f1)。 这也是很简单的,因为编译器为每个虚函数在 vtbl 内分配了一个唯一的索引。这一步的代价只是在 vtbl 数组内的一个偏移。

调用第二步找到的的指针所指向的函数。如果我们假设每个对象有一个隐藏的数据叫做 vptr,而且 f1 在 vtbl 中的索引为 i,此语句pC1->f1();

生成的代码就是这样的
(*pC1->vptr[i])(pC1); //调用被 vtbl 中第 i 个单元指向的函数,而                       

//pC1->vptr指向的是 vtbl;pC1 被做为 this 指针//传递给函数。

 

虚函数的实现机制付出的代价

不要将虚函数声明为 inline ,因为虚函数是运行时绑定的,而 inline 是编译时展开的,即使你对虚函数使用 inline ,编译器也通常会忽略。

虚函数的第二个成本:必须为每个拥有虚函数的类的对象,付出一个指针的代价,即 vptr ,它是一个隐藏的 data member,用来指向所属类的 vtbl。

调用一个虚函数的成本,基本上和通过一个函数指针调用函数相同,虚函数本身并不构成性能上的瓶颈。

 虚函数的第三个成本:事实上等于放弃了 inline。(如果虚函数是通过对象被调用,倒是可以 inline,不过一般都是通过对象的指针或引用调用的)

#include <iostream>
struct B1 
{ 
virtual void fun1(){} 
int id;
};
struct B2 
{ 
virtual void fun2(){} 
};
struct B3 
{ 
virtual void fun3(){} 
};
struct D : virtual B1, virtual B2, virtual B3
 {
virtual void fun(){}  
void fun1(){} 
       void fun2(){}   
void fun3(){}
};
int main()
{
    std::cout<<sizeof(B1)<<std::endl;   //8
    std::cout<<sizeof(B2)<<std::endl;   //4
    std::cout<<sizeof(B3)<<std::endl;   //4
std::cout<<sizeof(D)<<std::endl;    //16
}
//D 中只包含了三个 vptr ,D和B1共享了一个。
  • 0
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值