C++多态分析:虚函数调用是如何实现的?

什么是虚函数?

  1. 简单来说,虚函数是动态调用。相比于一般的函数调用在编译期确定了函数地址,而调用虚函数是在运行时决定调用的函数地址。
  2. 虚函数怎么使用相信大家都比较清楚,这里简单带过一下。C++中父类的指针可以指向子类实例,通过父类指针调用虚函数时会因为指向的不同的实例类型来调用不同的函数。
  3. C++多态性主要体现在虚函数上,某种程度上来说也只体现在虚函数上。(泛型是否属于多态这种PL问题我并不能准确回答。)

虚函数是如何实现的?

  1. 虚表指针(vfptr)
  2. 虚函数表(vtable)
  3. 动态调用。

虚表指针

在单继承情况下,如果父类存在虚函数,子类实例首地址开始4字节(在32位编译器下)会用来存放虚表指针。比如下面这两个类:

struct base {
    int x;
    virtual void func1() {
        printf("base func1\n");
    }
};
struct sub:base {
    int y;
};

在这个例子中,父类和子类分别拥有一个4字节成员。如果没有虚函数的情况下,sub类型所占的空间应该是8字节,但是因为父类中存在虚函数,就需要额外4字节用来存放虚表指针,所以用sizeof(sub)返回的结果会是12字节。

虚函数表

在sub实例开始的4字节所指向的就是虚函数表,这个表可以认为是一个由函数指针组成的数组。现在我们来看一下刚才示例中两个类的虚表,下面是base实例对象的内存,前4个字节就是虚表指针,后四个字节是成员x,因为debug没有初始化所以是CC。

base实例对象(其中00420F94指向的就是base的虚表, 虚表只有一个元素就是base::func1的指针
0040101E):

x86内存中因为是小端存储,所以“看起来”是反的,需要肉眼parse...

0019FF38  94 0F 42 00  // 虚表指针 
0019FF3C  CC CC CC CC

base的虚表:

00420F94  1E 10 40 00 // base::func1的函数指针

base::func1函数:

0040101E E9 4D 00 00 00       jmp         base::func1 (00401070)

sub的虚表内容相信大家也已经想到了,虽然在内存中这是两张表,但是因为sub并没有重写任何虚函数,所以虚表的内容和base是完全一样的,都是只有一个指向base::func1的函数指针。
sub的虚表:

0042003C  1E 10 40 00 // 和base虚表内容一样指向base::func1
示例2(重写了父类虚函数):

写到这里我发现刚刚的例子可能不太好,因为子类中没有重写虚函数,而且只有一个虚函数,没有直观的体现出作用。现在让我们来丰富一下刚才的两个类。

struct base {
    int x;
    virtual void func1() {
        printf("base func1\n");
    }
    virtual void func2() {
        printf("base func2\n");
    }
};
struct sub:base {
    int y;
    void func1() {
        printf("sub func1\n");
    }
    void func4() {
        printf("sub func4\n");
    }
};
void vPrint(base* ptr) {
    ptr->func1(); // 通过虚表指针动态调用函数
}

在这个例子中,sub重写了父类的func1函数。并且我们也可以通过传给vPrint不同类型的指针,来调用不同的函数:

int  main() {
    base b;
    vPrint(&b);
    sub s;
    vPrint(&s);
    return 0;
} 

输出如下:

base func1
sub func1

这个效果和我们想要的一样,现在我们再看一下sub的虚函数表。
sub的虚表:

00420058  6E 10 40 00  // sub::func1的指针
0042005C  5A 10 40 00 // base::func2的指针
0040105A E9 11 03 00 00       jmp         base::func2 (00401370)
0040106E E9 AD 03 00 00       jmp         sub::func1 (00401420)

可以看到因为重写了func1,所以虚表中第一个位置换成了sub::func1。func2没有被重写,所以还是和父类一样指向base::func2. 而func4因为没有被声明为虚函数,所以不会在虚表中存在。(func1虽然在子类中没有声明确声明为虚函数,但是C++中规定与父类虚函数同名的函数都会自动被声明为虚函数,当然这个同名指的是包括整个函数名、返回值、参数列表。)

base的虚表就不贴出来了,因为base没有继承其他类,所以base的虚表中只能是func1和func2两个函数的指针。

动态调用

说了这么久的虚表指针和虚表,现在终于可以看看是如何使用它们来实现动态调用的。
让我们来看一下vPrint函数的反汇编:

mov         eax,dword ptr [ebp+8]  // ebp+8是vPrint函数的第一个参数base* ptr
mov         edx,dword ptr [eax]    // 将实例对象的前4个字节,也就是虚表指针放在edx中
mov         ecx,dword ptr [ebp+8]  // 传参this指针,不用管
call        dword ptr [edx]        // call虚表中的第一个元素,根据传进来的对象的虚表不同,调用不同的函数

可以清晰的看到call的函数地址并不是一个立即数,而是edx指向的数据。这就是动态调用实现的关键了,根据对象中携带的虚表指针,来调用不同对象关联的不同函数。

总结

虚函数调用是通过call虚表数据来实现运行时调用。传递的参数类型不同,虚表指针就不同、虚表指针不同,指向的虚函数表就不同、虚函数表不同,指向的函数就不同。

附言

这篇文章中只讲了单继承的情况,而在多继承的情况下子类对象实例中就会有多个虚表指针,为了不使文章太过冗长,就不一一列出来了。

大家感兴趣的可以自己动手试一下,看看多继承的情况下内存分布如何,在传参时的偏移如何。

赵克,写于2017年01月13日。


如需转载请与我联系,并注明出处。

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

推荐阅读更多精彩内容

  • 原文地址:C语言函数调用栈(一)C语言函数调用栈(二) 0 引言 程序的执行过程可看作连续的函数调用。当一个函数执...
    小猪啊呜阅读 4,583评论 1 19
  • 一个博客,这个博客记录了他读这本书的笔记,总结得不错。《深度探索C++对象模型》笔记汇总 1. C++对象模型与内...
    Mr希灵阅读 5,563评论 0 13
  • 1. Java基础部分 基础部分的顺序:基本语法,类相关的语法,内部类的语法,继承相关的语法,异常的语法,线程的语...
    子非鱼_t_阅读 31,560评论 18 399
  • 1. C++基础知识点 1.1 有符号类型和无符号类型 当我们赋给无符号类型一个超出它表示范围的值时,结果是初始值...
    Mr希灵阅读 17,926评论 3 82
  • C++虚函数 C++虚函数是多态性实现的重要方式,当某个虚函数通过指针或者引用调用时,编译器产生的代码直到运行时才...
    小白将阅读 1,725评论 4 19