虚函数实现机制

说到虚函数的实现方法,我们就不得不说到动态联编(dynamic binding)和静态联编(static binding)。静态联编意味着编译器能够直接将标识符和存储的物理地址联系在一起。每一个函数都有一个唯一的物理地址,当编译器遇到一个函数调用时,它将用一个机械语言说明来替代函数调用,用来告诉CPU跳至这个函数的地址,然后对此函数进行操作。这个过程是在编译过程中完成的(注:调用的函数在编译时必须能够确定),所以静态联编也叫前期联编(early binding)。但是,如果使用哪个函数不能在编译时确定,则需要采用动态联编的方式,在程序运行时在调用函数,所以动态联编也叫后期联编(late binding)。

在C++继承多态中,如若要在派生类中重新定义基类的方法,则要把它声明为虚函数,并且用指针或者引用去调用它的方法,实现动态联编,否则编译器默认的将是静态联编。小看一下这个例子:

Example1:


#include <iostream>
using namespace std;

class A
{
    public:
    void f() { cout << "A" << endl; } //注意此处的函数不是虚函数

};

class B : public A

    public:
        void f() { cout << "B" << endl;}
};
int main (void)
{

A     a, *pa; 
    B     b;  
        

      a = b; //将子类对象赋给基类对象
      a.f();

      pa = &b; //用子类的对象的地址给基类指针初始化(符合赋值兼容规则)

      pa->f();

       

      return 0;
}

 

运行结果:


A

A

原因:编译器默认为静态联编方式,所以函数f(),在编译过程中就已经定死了,在子类中尽管你重新定义了f()的方法,但是编译器不知道应该调用哪个函数,所以就只会用的静态联编时的函数方法。

Example 2:


#include <iostream>
using namespace std;

class A
{
    public:
    virtual void f() { cout << "A" << endl; } //注意此处声明了虚函数

};

class B : public A
{    
    public:
        void f()    { cout << "B" << endl;}
};
int main (void)
{
    A     a, *pa; 
    B    b;
       

        a = b; //将子类对象赋给基类对象,这样做不能实现动态联编,虚函数特性失效
        a.f();
        b.f();
        pa = &b;
        pa->f();
        A &aa = b; //定义成引用类型也是可以的
        aa.f ();

 

        return 0;
}

运行结果:


A
B
B
B

先总结一下两个程序,若基类不声明为虚函数,则在A   a = b时,或者 A &a = b, A  *a = &b,创建并初始化对象时,是根据引用或指针的类型来选择方法的。这也就是我们第一个程序运行的结果的具体解释(觉得自己上面说的静态联编有点抽象,不知道这样说明会不会更容易理解一些。。O(∩_∩)O~)。若使用了virtual声明为虚函数,则程序将根据引用或指针指向的对像的类型来选择方法。这就程序二运行结果的原因。

对比两个结果就能很清楚的看到虚函数的作用。但是它具体的实现原理是什么呢?

C++采用了动态联编的一种特殊形式去实现虚函数,称为虚函数表。虚函数表是一张函数查找表,用以解决以动态联编方式调用函数。它为每个可以被类对象调用的虚函数提供一个入口,这样当我们用基类的指针或者引用来操作子类的对象时,这张虚函数表就提供了编译器实际调用的函数。虚函数表其实是存储了为类对象进行声明的虚函数地址。当我们创建一个类对象时,编译器会自动的生成一个指针*__vptr(一个隐藏指针),该指针指向这个类中所有虚函数的地址表。(实际上,虚函数表就是一个函数地址数组表。),请注意,*__vptr和*this指针不同,*this是一个被编译器用作解决自引用的函数参数,而*__vptr则是一个真正的指针。

每一个类,不管是基类还是子类都有一个自己的virtual table,而*__vptr也是被继承过来的。

我们再看一个例子:

Example:


#include <iostream>
using namespace std;

class A
{
    public:
     virtual void f()   { cout << "A’s f()" << endl; } //f()被声明为虚函数
      virtual void g()   { cout << "A’s g()"<< endl; } //g()被声明为虚函数 

};

class B : public A
{    
    public:
        void f()    { cout << "B’s f()" << endl; }
 };

class C : public A
{
    public:
          void g()    { cout << "C’s g()" << endl; }
};
int main (void)
{
        A     *pa;
        B     b;
        C     c;
        
        pa = &b;
        pa -> f();
        pa -> g();
        pa = &c;
        pa -> f();
        pa -> g();

return 0;
}

运行结果:


B’s f()
A’s g()
A’s f()
C’s g()

这个程序就能够反映出虚函数是怎样通过virtual table实现的,自己绘了一张图:应该能比较清楚的反映情况(借鉴于learnCpp.com)

1、c++实现多态的方法

其实很多人都知道,虚函数在c++中的实现机制就是用虚表和虚指针,但是具体是怎样的呢?从more effecive c++其中一篇文章里面可以知道:是每个类用了一个虚表,每个类的对象用了一个虚指针。具体的用法如下:

class A
{
public:
    virtual void f();
    virtual void g();
private:
    int a
};

class B : public A
{
public:
    void g();
private:
    int b;
};

//A,B的实现省略

因为A有virtual void f(),和g(),所以编译器为A类准备了一个虚表vtableA,内容如下:

A::f 的地址
A::g 的地址

B因为继承了A,所以编译器也为B准备了一个虚表vtableB,内容如下:

A::f 的地址
B::g 的地址

注意:因为B::g是重写了的,所以B的虚表的g放的是B::g的入口地址,但是f是从上面的A继承下来的,所以f的地址是A::f的入口地址。

然后某处有语句 B bB;的时候,编译器分配空间时,除了A的int a,B的成员int b;以外,还分配了一个虚指针vptr,指向B的虚表vtableB,bB的布局如下:

vptr : 指向B的虚表vtableB
int a: 继承A的成员
int b: B成员

当如下语句的时候:
A *pa = &bB;

pa的结构就是A的布局(就是说用pa只能访问的到bB对象的前两项,访问不到第三项int b)

那么pa->g()中,编译器知道的是,g是一个声明为virtual的成员函数,而且其入口地址放在表格(无论是vtalbeA表还是vtalbeB表)的第2项,那么编译器编译这条语句的时候就如是转换:call *(pa->vptr)[1](C语言的数组索引从0开始哈~)。

这一项放的是B::g()的入口地址,则就实现了多态。(注意bB的vptr指向的是B的虚表vtableB)

另外要注意的是,如上的实现并不是唯一的,C++标准只要求用这种机制实现多态,至于虚指针vptr到底放在一个对象布局的哪里,标准没有要求,每个编译器自己决定。我以上的结果是根据g++ 4.3.4经过反汇编分析出来的。

参考:http://blog.chinaunix.net/uid-24178783-id-370328.html

http://blog.csdn.net/jiangnanyouzi/article/details/3720807

时间: 2024-11-05 22:56:58

虚函数实现机制的相关文章

C++虚函数运行机制

C++ 虚函数表解析 前言 C++中的虚函数的作用主要是实现了多态的机制.关于多态,简而言之就是用父类型别的指针指向其子类的实例,然后通过父类的指针调用实际子类的成员函数.这种技术可以让父类的指针有"多种形态",这是一种泛型技术.所谓泛型技术,说白了就是试图使用不变的代码来实现可变的算法.比如:模板技术,RTTI技术,虚函数技术,要么是试图做到在编译时决议,要么试图做到运行时决议. 关于虚函数的使用方法,我在这里不做过多的阐述.大家可以看看相关的C++的书籍.在这篇文章中,我只想从虚函

C++虚函数实现机制

C++中虚函数的实现机制主要是VTable和虚指针.详细如下: class A { public: virtual void f1(); virtual void f2(); private: int a; } class B { public: void f1(); private: int b; } 如上A,B两个类,编译器为A类准备了一个虚表VTableA如下: A::f1的地址 A::f2的地址 编译器为B类准备的VTableB如下:    B::f1的地址    A::f2的地址 B类

虚函数运行机制

class A { public: virtual void foo (void) { ... } virtual void bar (void) { ... } }; class B : public A { public: void foo (void) { ... } }; int main(){ A* pa = new A; pa->foo (); // A::foo pa->bar (); // A::bar --------------------- A* pa = new B;

匹夫细说C#:从园友留言到动手实现C#虚函数机制

前言 上一篇文章匹夫通过CIL代码简析了一下C#函数调用的话题.虽然点击进来的童鞋并不如匹夫预料的那么多,但也还是有一些挺有质量的来自园友的回复.这不,就有一个园友提出了这样一个代码,这段代码如果被编译成CIL代码的话,对虚函数的调用会使用call而非callvirt: override string ToString() { return Base.ToString(); } 至于为何是这样,匹夫在回复中也做了解释,因为上面那段代码其实相当于是这样的: override string ToSt

C++ 虚函数和虚继承浅析

本文针对C++里的虚函数,虚继承表现和原理进行一些简单分析,有希望对大家学习C++有所帮助.下面都是以VC2008编译器对这两种机制内部实现为例. 虚函数 以下是百度百科对于虚函数的解释: 定义:在某基类中声明为 virtual 并在一个或多个派生类中被重新定 义的成员函数[1] 语法:virtual 函数返回类型 函数名(参数表) { 函数体 } 用途:实现多态性,通过指向派生类的基类指针,访问派生类中同名覆盖成员函数 函数声明和定义和普通的类成员函数一样,只是在返回值之前加入了关键字"vir

C++ 虚函数 、纯虚函数、接口的实用方法和意义

也许之前我很少写代码,更很少写面向对象的代码,即使有写多半也很容易写回到面向过程的老路上去.在写面向过程的代码的时候,根本不管什么函数重载和覆盖,想到要什么功能就变得法子的换个函数名字,心里想想:反正函数重载本质也就是入栈了两个不同的函数. 回过头来讲,让我了解标题这三个概念的实际用处,还是在于我这第四次重写毕业论文的代码,将它改写成面向对象的时候,才理解的.在面向对象设计的过程中, 类是从抽象逐渐具体起来的,父类可以是非常非常抽象的东西,而最终实例化的子类就非常具体了.在这个继承的过程中,不断

虚函数和模板编程的一点共性和特征模板的一个例子

最近在看元编程中,对虚函数和模板编程有一点点感悟,写一篇博客简单总结一下. 虚函数和模板是C++里面很棒的特征,他们都提供了一种方法,让程序在编译中完成一些计算,去掉的这些计算在比较low的编程方式中,是需要在程序运行中执行的.在这里,我要强调的是:"在编译过程中完成一些计算". 我会举两个例子,一个是虚函数的,比较简单,另一个例子是关于特征模板的,在例子中,根据模板参数的类型自动选择模板的底层数据结构. 第一个例子是比较简单的虚函数的例子,有很多种水果的类型,我们有一个函数要展示他们

虚函数与虚继承小结

  虚函数的作用就是实现多态性,通过指向派生类的基类指针或引用,访问派生类中同名覆盖成员函数:实现方法就是在函数返回值之前加上关键字"virtual":如下: #include <stdio.h> class A { public: void fn() { printf("fn in A\n"); } virtual void v_fn() { printf("virtual fn in A\n"); } }; class B : p

C++手稿:虚函数与多态

C++类继承带来了诸多好处:基类代码复用.通用的方法和属性.更好的可维护性, 然而最大的好处莫过于提供统一的接口.接口是一种对类型的抽象,它统一了一系列类的行为, 不同类的对象之间交互更加容易.Java.objective C等面向对象语言都提供了接口的概念, 在C++中,可以通过抽象类来实现一个接口. C++通过虚函数实现了多态:通过基类指针或引用调用虚函数时,会调用当前对象的实际类型中声明的函数. 为了这个特性,包含虚函数的C++对象中会存储一个虚函数表指针,来完成动态联编. 编译程序在编译