理解 C++ 虚函数表

引言

虚表是 C++ 中一个十分重要的概念,面向对象编程的多态性在 C++ 中的实现全靠虚表来实现。在聊虚表之前我们先回顾一下什么事多态性。

多态实际上就是让一个父类指针,通过赋予子类对象的地址,可以呈现出多种形态和功能。如果这么说比较抽象的话,我们看一个例子就明白了:

class Base {
    int m_tag;
public:
    Base(int tag) : m_tag(tag) {}
    
    void print() {
        cout << "Base::print() called" << endl;
    }
    
    virtual void vPrint() {
        cout << "Base::vPrint() called" << endl;
    }
    
    virtual void printTag() {
        cout << "Base::m_tag of this instance is: " << m_tag << endl;
    }
};

class Derived : public Base {
public:
    Derived(int tag) : Base(tag) {}
    
    void print() {
        cout << "Derived1::print() called" << endl;
    }
    
    virtual void vPrint() {
        cout << "Derived::vPrint() called" << endl;
    }
};

在上面的代码中,我们声明了一个父类 Base,和它的一个派生类 Derived,其中 print() 实例方法是非虚函数,而其余两个实例方法被声明为了虚函数。并且在子类中我们重新了 print()vPrint()。下面我们构造出一个 Derived 实例,并分别将其地址赋给 Base 指针和 Derived 指针:

int main(int argc, char *argv[]) {
    Derived *foo = new Derived(1);
    Base *bar = foo;
    
    foo->print();
    foo->vPrint();
    
    bar->print();
    bar->vPrint();
    
    return 0;
}

我们看看程序运行的结果:

Derived1::print() called
Derived::vPrint() called
Base::print() called
Derived::vPrint() called

可以看到,对于 Derived 指针的操作正如它应该表现的样子,然而当我们把相同对象的地址赋给 Base 指针时,可以发现它的非虚函数竟然表现出了父类的行为,并没有被重写的样子。

这是什么原因呢?

C++ 类的实质是什么

首先我们要明白 C++ 中类的实质到底是什么。实际上,类在 C++ 中就是 struct (结构体)的一种扩展,允许了更高级的继承和虚函数。那么也就是说,结构体缺少的实际上就是虚函数。

对于一般的成员变量,它和结构体在内存布局上是完全一样的,不管是顺序还是内存对齐,完全一致。而一个类的方法地址并不会存储在一个实例的内存中。对于非虚函数,它们在内存中的地址是唯一的,你可以把它想象成普通函数,只不过第一个参数是 this 指针,在通过类对象指针调用时,编译器会根据类型找到相应非虚函数的地址,这个工作是编译时完成的。

也就是说,什么指针指向什么函数这是固定的,反正指针如果是 Base *,那我就直接执行 Base::print() 函数。

揭开 vTable 的神秘面纱

既然非虚函数实现这么简单,那虚函数是不是会很复杂?其实并不是那么复杂。虚函数的地址被存储一张叫做虚表的东西里,我们其实很容易拿到这个虚表。下面我们通过 dump memory 的方式来揪出一个类的虚表:

看到我选中的那个字节,那是我们的一个实例变量,在这个实例变量的前面有 8 个字节的内容,那实际就是虚表的地址了,我们尝试将这个地址所指向的内容拿出来:

这就是虚表的内容了,什么?你不信,下面我就把虚表中第一个函数揪出来执行一下:


可以看到,Derived 类中重写的 vPrint() 方法已经被执行。这就说明虚函数在执行时是一个动态的过程,并不是在编译时就确定下来要执行哪一个函数,而是运行时从虚表查到真正要执行的函数的地址,然后再将 this 指针传入执行。

到这里,我们已经大致了解了虚函数是怎样工作的了。下面我们看看 Base 类和 Derived 类的虚表有什么区别。我修改了源码,实例化了一个 Base 类对象 baz,然后分别 dump 出 Base 类和 Derived 类的内存:

可以看出,两个对象的虚表指针是不同的。然后我们看看这两者虚表有什么不同:

这两张虚表的第一个函数不同,因为 Derived 类重写了 vPrint() 方法,所以 Derived 的虚表第一个函数指针会有不同,而 printTag() 我并没有重写,所以两张表指向一个同一个函数。

所以每个类都会维护一张虚表,编译时,编译器根据类的声明创建出虚表,当对象被构造时,虚表的地址就会被写入这个对象内存的起始位置。这就是多态性在 C++ 中实现的方式,而像 Java、OC 这样的语言由于 Runtime 的存在,这些对象会有多余的内存空间记录类的信息(meta-object),在运行时根据这些信息解析出相应的函数去执行。虽然不同,但是异曲同工。

理解虚函数表有什么作用呢?

  • 能让你更好地理解 C++
  • 一些 hook 技术就是利用虚表来实现的

Wrap Up

这篇文章就简单地讲了一下多态和虚函数在 C++ 中的实现,我们说 C++ 非常 magical 就是因为它能用最简单的方式去实现各种面向对象编程的特性,十分值得我们终身学习。

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

推荐阅读更多精彩内容

  • 一个博客,这个博客记录了他读这本书的笔记,总结得不错。《深度探索C++对象模型》笔记汇总 1. C++对象模型与内...
    Mr希灵阅读 5,566评论 0 13
  • 1. C++基础知识点 1.1 有符号类型和无符号类型 当我们赋给无符号类型一个超出它表示范围的值时,结果是初始值...
    Mr希灵阅读 17,935评论 3 82
  • 1. 结构体和共同体的区别。 定义: 结构体struct:把不同类型的数据组合成一个整体,自定义类型。共同体uni...
    breakfy阅读 2,112评论 0 22
  • 这篇日志中的路由选择过程,是以ping包为例。ICMP协议请求应答只涉及到网络层、数据链路层以及物理层。不涉及第四...
    初心不渝阅读 1,398评论 1 0
  • 1.你在为谁工作 你一生中所犯的最大错误,就是认为你是在为别人工作,而不是为自己工作。其实,从你的第一份工作...
    江湖人称贾老师阅读 770评论 0 51