C++虚函数表

虚函数表

C++中虚函数是通过一张虚函数表(Virtual Table)来实现的,在这个表中,主要是一个类的虚函数表的地址表;这张表解决了继承、覆盖的问题。在有虚函数的类的实例中这个表被分配在了这个实例的内存中,所以当我们用父类的指针来操作一个子类的时候,这张虚函数表就像一张地图一样指明了实际所应该调用的函数。

C++编译器是保证虚函数表的指针存在于对象实例中最前面的位置(是为了保证取到虚函数表的最高的性能),这样我们就能通过已经实例化的对象的地址得到这张虚函数表,再遍历其中的函数指针,并调用相应的函数。

下面先看一段代码:

class Base {
public:
    virtual void f() { cout << "Base::f()" << endl; }
    virtual void g() { cout << "Base::g()" << endl; }
    virtual void h() { cout << "Base::h()" << endl; }
};

typedef void(*Fun)(void);

int main()
{
    Base b;
    Fun pFun = NULL;
    cout << "虚函数表的地址为:" << (int*)(&b) << endl;
    cout << "虚函数表的第一个函数地址为:" << (int*)*(int*)(&b) << endl;

    pFun = (Fun)*((int*)*(int*)(&b));
    pFun();
    system("pause");
    return 0;
}

运行结果如下:


TIM截图20181030142610.png

我们再追踪一下虚函数表的地址:

image.png
image.png
image.png

结合结果分析一下代码:首先是创建了一个Base的类,Base类里面有三个成员函数,都为虚函数;然后typedef void(*Fun)(void)是利用类型别名声明一个函数指针,指向的地址为NULL,可以等价成这样:typedef decltype(void) *Fun。然后再到main函数里,利用Base实例化了对象了b;然后Fun pFun=NULL则是声明了一个返回指向函数的指针,该指针pFun此时也是NULL,根据图1可以知道,他的类型是void(*)(),表示的就是函数指针,而当执行完这句后就会从原来没有分配的0xcccccccc变成0x00000000。接下来就是(int*)(&b)强行把&b转成int *,取得虚函数表的地址;再次取址就可以得到第一个虚函数的地址了,也就是Base::f() 。接下来就是pFun = (Fun)*((int*)*(int*)(&b));把函数指针指向虚函数表的第一个函数,最后再pFunc()运行。

如果想让pFun调用其它的函数,可以是这样:

(Fun)*((int*)*(int*)(&b)+0);  // Base::f()

(Fun)*((int*)*(int*)(&b)+1);  // Base::g()

(Fun)*((int*)*(int*)(&b)+2);  // Base::h()

通过下图可以很好的进行理解:

image.png

(int*)*(int*)(&b)可以这样理解,(int*)(&b)就是对象b的地址,只不过被强制转换成了int*了,如果直接调用*(int*)(&b)则是指向对象b地址所指向的数据,但是此处是个虚函数表呀,所以指不过去,必须通过(int*)将其转换成函数指针来进行指向就不一样了,它的指向就变成了对象b中第一个函数的地址,所以(int*)*(int*)(&b)就是独享b中第一个函数的地址;又因为pFun是由Fun这个函数声明的函数指针,所以相当于是Fun的实体,必须再将这个地址转换成pFun认识的,即加上(Fun)*进行强制转换:

image.png

下面将对比说明有无虚函数覆盖情况下的虚函数表的样子:

一般继承(无虚函数覆盖)

先写出一个继承关系:


image.png

写成代码如下:

class Base {
public:
    virtual void f() { cout << "Base::f()" << endl; }
    virtual void g() { cout << "Base::g()" << endl; }
    virtual void h() { cout << "Base::h()" << endl; }
};

class Derive :public Base{
public:
    virtual void f1() { cout << "Base::f1()" << endl; }
    virtual void g1() { cout << "Base::g1()" << endl; }
    virtual void h1() { cout << "Base::h1()" << endl; }
};

typedef void(*Fun)(void);

int main()
{
    //Base b;
    Derive d;
    Fun pFun = NULL;
    cout << "虚函数表的地址为:" << (int*)(&d) << endl;
    cout << "虚函数表的第一个函数地址为:" << (int*)*(int*)(&d)<< endl;
    pFun =(Fun)*((int*)*(int*)(&d)+1);
    pFun();
    pFun = (Fun)*((int*)*(int*)(&d) + 3);
    pFun();

    system("pause");
    return 0;
}

执行结果如下:

image.png

通过调试看一下相应的虚函数表:

image.png
image.png

这个继承关系中,子类没有重载任何父类的函数,我们实例化了一个对象d:Derive d,则它的虚函数表是如下的:

image.png

所以虚函数按照其声明顺序放于表中,并且父类的虚函数在子类的虚函数前面

一般继承(有虚函数覆盖)

这样的一个继承关系:

image.png

这个继承关系中,Derive的f()重载了Base类中的f(),下面我们来逐步调试:

[图片上传失败...(image-f2104c-1540906440228)]

上图是刚刚通过Derive d声明的虚函数表的样子,我们再直接打印出对象d的第一个函数、第三个函数和第四个函数来看看:

image.png

相应的虚函数表变成了:

image.png

我们可以知道覆盖的f()函数被放到了虚函数表中原来父类虚函数的位置,而没有被覆盖的函数依次往后排列

多重继承(无虚函数覆盖)

现在用这样一个继承关系:

image.png

调试程序发现:

image.png

如果我们访问第一个函数地址之后的第6个函数位置会发生什么呢?

image.png

是找不到的,说明虚函数表已经不是按原来的方式通过一个地址找到所有的函数,或者说所有的子函数实现是按照顺序排列来存放的了。

虚函数表是这样的:

image.png

但是我们通过我们目前实现访问虚函数表的方式是访问不到下面两张虚函数表的,却可以通过这样来实现:

Derive d;
Base2 *b2=&d;
b2->f();
b2->g();
image.png
image.png

在声明了b2并绑定d的时候,已经指向了Base2的虚函数表的地址,再通过b2->f()就可以访问Base2的虚函数表中第一个函数的位置。

多重继承(有虚函数覆盖)

继承关系:

image.png

在这里就不放出代码和调试内容了,直接给出虚函数表的样子:

image.png

几点注意

1.不能通过父类型的指针访问子类自己的虚函数,是非法的

Base *b=new Derive();
b->f1();     //编译会出错

P.S:我们可以通过指针的方式访问虚函数表来达到违反C++语义的行为

class Base1 {
public:
    virtual void f() { cout << "Base1::f" << endl; }
    virtual void g() { cout << "Base1::g" << endl; }
    virtual void h() { cout << "Base1::h" << endl; }
};

class Base2 {
public:
    virtual void f() { cout << "Base2::f" << endl; }
    virtual void g() { cout << "Base2::g" << endl; }
    virtual void h() { cout << "Base2::h" << endl; }
};

class Base3 {
public:
    virtual void f() { cout << "Base3::f" << endl; }
    virtual void g() { cout << "Base3::g" << endl; }
    virtual void h() { cout << "Base3::h" << endl; }
};

class Derive : public Base1, public Base2, public Base3 {
public:
    virtual void f() { cout << "Derive::f" << endl; }
    virtual void g1() { cout << "Derive::g1" << endl; }
};

typedef void(*Fun)(void);

int main()
{
    Fun pFun = NULL;
    Derive d;
    int** pVtab = (int**)&d;
    //Base1's vtable
    //pFun = (Fun)*((int*)*(int*)((int*)&d+0)+0);
    pFun = (Fun)pVtab[0][0];
    pFun();
    //pFun = (Fun)*((int*)*(int*)((int*)&d+0)+1);
    pFun = (Fun)pVtab[0][1];
    pFun();
    //pFun = (Fun)*((int*)*(int*)((int*)&d+0)+2);
    pFun = (Fun)pVtab[0][2];
    pFun();
    //Derive's vtable
    //pFun = (Fun)*((int*)*(int*)((int*)&d+0)+3);
    pFun = (Fun)pVtab[0][3];
    pFun();
    //The tail of the vtable
    pFun = (Fun)pVtab[0][4];
    cout << pFun << endl;
    //Base2's vtable
    //pFun = (Fun)*((int*)*(int*)((int*)&d+1)+0);
    pFun = (Fun)pVtab[1][0];
    pFun();
    //pFun = (Fun)*((int*)*(int*)((int*)&d+1)+1);
    pFun = (Fun)pVtab[1][1];
    pFun();
    pFun = (Fun)pVtab[1][2];
    pFun();
    //The tail of the vtable
    pFun = (Fun)pVtab[1][3];
    cout << pFun << endl;

    //Base3's vtable
    //pFun = (Fun)*((int*)*(int*)((int*)&d+1)+0);
    pFun = (Fun)pVtab[2][0];
    pFun();
    //pFun = (Fun)*((int*)*(int*)((int*)&d+1)+1);
    pFun = (Fun)pVtab[2][1];
    pFun();
    pFun = (Fun)pVtab[2][2];
    pFun();
    //The tail of the vtable
    pFun = (Fun)pVtab[2][3];
    cout << pFun << endl;
    return 0;

}

在这里延伸一下关于int **int *,这里引用了知乎上的一片答案:

C语言里面的定义的指针,它除了表示一个地址,它还带有类型信息。这个类型信息,用来告诉你,在这个地址空间上,存放着什么类型的变量。打个比如,有如下的代码片段:int a;
int *p = &a;
假设p的指针值为0x08004000,并且int类型长度为4字节。那么p将告诉你,[0x08004000, 0x08004004) 内存空间上存放着一个int类型。如果你只从0x08004000地址中,只读取两个字节,那是错误的。同样道理,以下代码片段:int a;
int *b = &a;
int **c = &b;
是告诉你,c的指针值是另一个指针(b),而该指针则指向一个int变量。如果你刻意要较真,认为指针就是一个地址,并且举出下面的例子:int a;
int *b = &a;
int *c = &b;  // 请留意这里
最后一句赋值,实际上是将类型信息丢弃了。因为在编译器看来,C 就表示一个指针,它指向的是一个整数,只是你自己将它解释成指针而已。printf("%d\n",*(int *)(*c)); // 开发人员,将*c解释成指针,编译器是不认帐的喔
为什么将一个整数解释成一个指针没有问题呢?那是因为凑巧,你把它放到64位架构上试试看看。

2.如果父类的虚函数是private或者是protected的,但这些非public的虚函数同样会存在于虚函数表中!
3.虚函数表不一定是存在最开头,但是目前各个编译器大多是这样设置的。

©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 214,172评论 6 493
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 91,346评论 3 389
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 159,788评论 0 349
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 57,299评论 1 288
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 66,409评论 6 386
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 50,467评论 1 292
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 39,476评论 3 412
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 38,262评论 0 269
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 44,699评论 1 307
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 36,994评论 2 328
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 39,167评论 1 343
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 34,827评论 4 337
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 40,499评论 3 322
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 31,149评论 0 21
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 32,387评论 1 267
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 47,028评论 2 365
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 44,055评论 2 352

推荐阅读更多精彩内容

  • 原帖地址:https://www.cnblogs.com/hushpa/p/5707475.html 2018.9...
    一念之动即是行阅读 721评论 0 1
  • //联系人:石虎QQ:1224614774昵称:嗡嘛呢叭咪哄 一、概念 1.多态是由虚函数实现的,而虚函数主要是通...
    石虎132阅读 1,597评论 0 10
  • 引言 虚表是 C++ 中一个十分重要的概念,面向对象编程的多态性在 C++ 中的实现全靠虚表来实现。在聊虚表之前我...
    Cyandev阅读 19,784评论 7 38
  • 1.C和C++的区别?C++的特性?面向对象编程的好处? 答:c++在c的基础上增添类,C是一个结构化语言,它的重...
    杰伦哎呦哎呦阅读 9,499评论 0 45
  • 一周检视# 处世之要:正其谊,不谋其利;明其道,不计其功!🌈 🌞【第5周的成就】 🌻健康 休息:早晨5:30分左右...
    黄宣瑞阅读 190评论 0 0