关键词:虚函数。虚表,虚表指针,动态绑定,多态

一、概述

为了实现C++的多态,C++使用了一种动态绑定的技术。

这个技术的核心是虚函数表(下文简称虚表)。本文介绍虚函数表是怎样实现动态绑定的。

二、类的虚表

每一个包括了虚函数的类都包括一个虚表。

我们知道,当一个类(A)继承还有一个类(B)时。类A会继承类B的函数的调用权。所以假设一个基类包括了虚函数,那么其继承类也可调用这些虚函数,换句话说,一个类继承了包括虚函数的基类。那么这个类也拥有自己的虚表。

我们来看下面的代码。

类A包括虚函数vfunc1。vfunc2。因为类A包括虚函数,故类A拥有一个虚表。

class A {
public:
virtual void vfunc1();
virtual void vfunc2();
void func1();
void func2();
private:
int m_data1, m_data2;
};

类A的虚表如图1所看到的。



图1:类A的虚表示意图

虚表是一个指针数组,其元素是虚函数的指针,每一个元素相应一个虚函数的函数指针。须要指出的是。普通的函数即非虚函数。其调用并不须要经过虚表,所以虚表的元素并不包括普通函数的函数指针。

虚表内的条目。即虚函数指针的赋值发生在编译器的编译阶段。也就是说在代码的编译阶段。虚表就能够构造出来了。

三、虚表指针

虚表是属于类的,而不是属于某个详细的对象,一个类仅仅须要一个虚表就可以。同一个类的全部对象都使用同一个虚表。

为了指定对象的虚表。对象内部包括一个虚表的指针,来指向自己所使用的虚表。为了让每一个包括虚表的类的对象都拥有一个虚表指针,编译器在类中加入了一个指针,*__vptr。用来指向虚表。这样,当类的对象在创建时便拥有了这个指针,且这个指针的值会自己主动被设置为指向类的虚表。



图2:对象与它的虚表

上面指出,一个继承类的基类假设包括虚函数,那个这个继承类也有拥有自己的虚表,故这个继承类的对象也包括一个虚表指针,用来指向它的虚表。

四、动态绑定

讲到这里,大家一定会好奇C++是怎样利用虚表和虚表指针来实现动态绑定的。我们先看下面的代码。

class A {
public:
virtual void vfunc1();
virtual void vfunc2();
void func1();
void func2();
private:
int m_data1, m_data2;
}; class B : public A {
public:
virtual void vfunc1();
void func1();
private:
int m_data3;
}; class C: public B {
public:
virtual void vfunc2();
void func2();
private:
int m_data1, m_data4;
};

类A是基类,类B继承类A。类C又继承类B。类A,类B,类C,其对象模型例如以下图3所看到的。



图3:类A,类B,类C的对象模型

因为这三个类都有虚函数,故编译器为每一个类都创建了一个虚表,即类A的虚表(A vtbl)。类B的虚表(B vtbl),类C的虚表(C vtbl)。类A,类B,类C的对象都拥有一个虚表指针,*__vptr,用来指向自己所属类的虚表。

类A包括两个虚函数,故A vtbl包括两个指针,分别指向A::vfunc1()和A::vfunc2()。

类B继承于类A。故类B能够调用类A的函数。但因为类B重写了B::vfunc1()函数,故B vtbl的两个指针分别指向B::vfunc1()和A::vfunc2()。

类C继承于类B,故类C能够调用类B的函数,但因为类C重写了C::vfunc2()函数,故C vtbl的两个指针分别指向B::vfunc1()(指向继承的近期的一个类的函数)和C::vfunc2()。

尽管图3看起来有点复杂,可是仅仅要抓住“对象的虚表指针用来指向自己所属类的虚表,虚表中的指针会指向其继承的近期的一个类的虚函数”这个特点,便能够高速将这几个类的对象模型在自己的脑海中描绘出来。

非虚函数的调用不用经过虚表。故不须要虚表中的指针指向这些函数。

假设我们定义一个类B的对象。因为bObject是类B的一个对象,故bObject包括一个虚表指针,指向类B的虚表。

int main()
{
B bObject;
}

如今,我们声明一个类A的指针p来指向对象bObject。

尽管p是基类的指针仅仅能指向基类的部分,可是虚表指针亦属于基类部分,所以p能够訪问到对象bObject的虚表指针。bObject的虚表指针指向类B的虚表,所以p能够訪问到B vtbl。

如图3所看到的。

int main()
{
B bObject;
A *p = & bObject;
}

当我们使用p来调用vfunc1()函数时,会发生什么现象?

int main()
{
B bObject;
A *p = & bObject;
p->vfunc1();
}

程序在执行p->vfunc1()时,会发现p是个指针。且调用的函数是虚函数,接下来便会进行下面的步骤。

首先,依据虚表指针p->__vptr来訪问对象bObject相应的虚表。尽管指针p是基类A*类型。可是*__vptr也是基类的一部分,所以能够通过p->__vptr能够訪问到对象相应的虚表。

然后,在虚表中查找所调用的函数相应的条目。因为虚表在编译阶段就能够构造出来了,所以能够依据所调用的函数定位到虚表中的相应条目。

对于 p->vfunc1()的调用,B vtbl的第一项即是vfunc1相应的条目。

最后,依据虚表中找到的函数指针。调用函数。从图3能够看到。B vtbl的第一项指向B::vfunc1()。所以 p->vfunc1()实质会调用B::vfunc1()函数。

假设p指向类A的对象,情况又是怎么样?

int main()
{
A aObject;
A *p = &aObject;
p->vfunc1();
}

当aObject在创建时,它的虚表指针__vptr已设置为指向A vtbl,这样p->__vptr就指向A vtbl。vfunc1在A vtbl相应在条目指向了A::vfunc1()函数,所以 p->vfunc1()实质会调用A::vfunc1()函数。

能够把以上三个调用函数的步骤用下面表达式来表示:

(*(p->__vptr)[n])(p)

能够看到。通过使用这些虚函数表。即使使用的是基类的指针来调用函数。也能够达到正确调用执行中实际对象的虚函数。

我们把经过虚表调用虚函数的过程称为动态绑定,其表现出来的现象称为执行时多态。动态绑定差别于传统的函数调用,传统的函数调用我们称之为静态绑定,即函数的调用在编译阶段就能够确定下来了。

那么,什么时候会执行函数的动态绑定?这须要符合下面三个条件。

  • 通过指针来调用函数
  • 指针upcast向上转型(继承类向基类的转换称为upcast,关于什么是upcast。能够參考本文的參考资料)
  • 调用的是虚函数

假设一个函数调用符合以上三个条件,编译器就会把该函数调用编译成动态绑定,其函数的调用过程走的是上述通过虚表的机制。

五、总结

封装。继承,多态是面向对象设计的三个特征,而多态能够说是面向对象设计的关键。C++通过虚函数表,实现了虚函数与对象的动态绑定。从而构建了C++面向对象程序设计的基石。

參考资料

附录

演示样例代码

C++虚函数表剖析的更多相关文章

  1. 【C++ Primer | 15】C++虚函数表剖析①

    概述 为了实现C++的多态,C++使用了一种动态绑定的技术.这个技术的核心是虚函数表(下文简称虚表).本文介绍虚函数表是如何实现动态绑定的. C++多态实现的原理: •  当类中声明虚函数时,编译器会 ...

  2. 【C++ Primer | 15】C++虚函数表剖析②

    多重继承 下面,再让我们来看看多重继承中的情况,假设有下面这样一个类的继承关系. 注意:子类只overwrite了父类的f()函数,而还有一个是自己的函数(我们这样做的目的是为了用g1()作为一个标记 ...

  3. 深入剖析C++多态、VPTR指针、虚函数表

    在讲多态之前,我们先来说说关于多态的一个基石------类型兼容性原则. 一.背景知识 1.类型兼容性原则 类型兼容规则是指在需要基类对象的任何地方,都可以使用公有派生类的对象来替代.通过公有继承,派 ...

  4. C++ 虚函数表解析

    转载:陈皓 http://blog.csdn.net/haoel 前言 C++中 的虚函数的作用主要是实现了多态的机制.关于多态,简而言之就是用父类型别的指针指向其子类的实例,然后通过父类的指针调用实 ...

  5. C++ 虚函数表解析(转载)

    转载自:陈皓 http://blog.csdn.net/haoel/article/details/1948051/ 前言 C++中的虚函数的作用主要是实现了多态的机制.关于多态,简而言之就是用父类型 ...

  6. 转载:C++ 虚函数表解析

    目录(?)[+]   转载:http://blog.csdn.net/haoel/article/details/1948051# 前言 C++中 的虚函数的作用主要是实现了多态的机制.关于多态,简而 ...

  7. C++虚函数表解析(转)

    C++中的虚函数的作用主要是实现了多态的机制.关于多态,简而言之就是用父类型别的指针指向其子类的实例,然后通过父类的指针调用实际子类的成员函数.这种技术可以让父类的指针有“多种形态”,这是一种泛型技术 ...

  8. C++ 虚函数表解析(比较清楚,还可打印虚函数地址)

    C++ 虚函数表解析 陈皓 http://blog.csdn.net/haoel 前言 C++中的虚函数的作用主要是实现了多态的机制.关于多态,简而言之就是用父类型别的指针指向其子类的实例,然后通过父 ...

  9. C++ 虚函数表决心

    C++ 虚函数表解析 xml:namespace prefix = o /> 陈皓 http://blog.csdn.net/haoel 前言 C++中的虚函数的作用主要是实现了多态的机制. 关 ...

随机推荐

  1. Jmeter之https请求

    Jmeter之录制https脚本,网上介绍了好多种方法,大家自行百度. 如果手写https脚本,该如何做呢? 方法:http信息头管理器,加入User-Agent参数 案例:手写百度的搜索:哈哈  请 ...

  2. close - 关闭一个文件描述符

    SYNOPSIS 总览 #include <unistd.h> int close(int fd); DESCRIPTION 描述 close 关闭 一个 文件 描述符 , 使它 不在 指 ...

  3. Perl字符集[\d\D]表示任何字符(所有数字和非数字,包括换行符),“.”表示除了换行符以外的所有字符。

    Perl字符集[\d\D]表示任何字符(所有数字和非数字,包括换行符),“.”表示除了换行符以外的所有字符.

  4. PHP 下基于 php-amqp 扩展的 RabbitMQ 简单用例 (一) -- 安装 AMQP 扩展和 Direct Exchange 模式

    Windows 安装 amqp 扩展 RabbitMQ 是基于 amqp(高级消息队列协议) 协议的.使用 RabbitMQ 前必须为 PHP 安装相应的 amqp 扩展. 下载相应版本的 amqp ...

  5. 【C语言】控制台窗口图形界面编程(一)句柄和文本属性

    目录 00. 目录 01. 句柄 02. GetStdHandle函数 03. CloseHandle函数 04. SetConsoleTextAttribute函数 05. 十进制颜色对照表 06. ...

  6. NET使用SuperSocket完成TCP/IP通信

    1)为什么使用SuperSocket? 性能高,易上手.有中文文档,我们可以有更多的时间用在业务逻辑上,SuperSocket有效的利用自己的协议解决粘包 2)SuperSocket的协议内容? 命令 ...

  7. IIS更改根目录

    服务器中打开IIS管理器,选择网站,再选展开后的站点,点击右边高级设置,最后更改弹框中的物理地址即可:

  8. vue多视图

    第一步   在app.vue中 <router-view class="b" name="header"> </router-view> ...

  9. js 循环 js创建数组

    循环 for (var i = 0; i < myArray.length; i++) { console.log(myArray[i]); }; for (var arr in myArray ...

  10. Dijkstra算法C++实现总结

    问题描述 求无负权图中点s到点t的最短凝聚力 备注 标准说法中,"缩短"/"松弛"(relax)操作是对边进行的.下面为了行文方便,将其拓展到点.即以下操作,其 ...