C++虚函数表

虚函数

对C++ 了解的人都应该知道虚函数(Virtual Function)是通过一张虚函数表(Virtual Table)来实现的。简称为V-Table。在这个表中,主是要一个类的虚函数的地址表,这张表解决了继承、覆盖的问题,保证其容真实反应实际的函数。这样,在有虚函数的类的实例中这个表被分配在了这个实例的内存中,所以,当我们用父类的指针来操作一个子类的时候,这张虚函数表就显得由为重要了,它就像一个地图一样,指明了实际所应该调用的函数。

这里我们着重看一下这张虚函数表。C++的编译器应该是保证虚函数表的指针存在于对象实例中最前面的位置(这是为了保证取到虚函数表的有最高的性能——如果有多层继承或是多重继承的情况下)。 这意味着我们通过对象实例的地址得到这张虚函数表,然后就可以遍历其中函数指针,并调用相应的函数。

虚函数表存在的位置

​ 由于虚函数表是由编译器给我们生成的,那么编译器会把虚函数表安插在哪个位置呢?下面可以简单的写一个示例来证明一下虚函数表的存在,以及观察它所存在的位置,先来看一下代码:

#include <iostream>
#include <stdio.h>
using namespace std;class A{
public:int x;virtual void b() {}
};int main()
{A* p = new A;cout << p << endl;cout << &p->x << endl;return 0;
}

定义了一个类A,含有一个x和一个虚函数,实例化一个对象,然后输出对象的地址和对象成员x的地址,我们想一下,如果对象的地址和x的地址相同,那么就意味着编译器把虚函数表放在了末尾,如果两个地址不同,那么就意味着虚函数表是放在最前面的。运行结果为16进制,然后我们把它转为10进制观察一下:

image-20210525230252139

可以观察到结果是不同的,而且正好相差了4个字节,由此可见,编译器把生成的虚函数表放在了最前面

获取虚函数表

既然虚函数表是真实存在的,那么我们能不能想办法获取到虚函数表呢?其实我们可以通过指针的形式去获得,因为前面也提到了,我们可以把虚函数表看作是一个数组,每一个单元用来存放虚函数的地址,那么当调用的时候可以直接通过指针去调用所需要的函数就行了。我们就类比这个思路,去获取一下虚函数表。首先先定义两个类,一个是基类一个是派生类,代码如下:

#include <iostream>
#include <stdio.h>
using namespace std;class Base {
public:virtual void a() { cout << "Base a()" << endl; }virtual void b() { cout << "Base b()" << endl; }virtual void c() { cout << "Base c()" << endl; }
};class Derive : public Base {
public:virtual void b() { cout << "Derive b()" << endl; }
};int main()
{cout << "-----------Base------------" << endl;Base* q = new Base;long* tmp1 = (long*)q;long* vptr1 = (long*)(*tmp1);for (int i = 0; i < 3; i++) {printf("vptr[%d] : %p\n", i, vptr1[i]);}Derive* p = new Derive;long* tmp = (long*)p;long* vptr = (long*)(*tmp);cout << "---------Derive------------" << endl;for (int i = 0; i < 3; i++) {printf("vptr[%d] : %p\n", i, vptr[i]);}return 0;
}

image-20210525230927706

可见基类中的三个指针分别指向a,b,c虚函数,而派生类中的三个指针中第一个和第三个和基类中的相同,那么这就印证了上述我们所假设的情况,那么这也就是虚函数表。但是仅仅只是观察指向的地址,还不是让我们观察的特别清楚,那么我们就通过定义函数指针,来调用一下这几个地址,看看结果是什么样的,下面直接上代码:

#include <iostream>
#include <stdio.h>
using namespace std;class Base {
public:virtual void a() { cout << "Base a()" << endl; }virtual void b() { cout << "Base b()" << endl; }virtual void c() { cout << "Base c()" << endl; }
};class Derive : public Base {
public:virtual void b() { cout << "Derive b()" << endl; }
};int main()
{typedef void (*Func)();cout << "-----------Base------------" << endl;Base* q = new Base;long* tmp1 = (long*)q;long* vptr1 = (long*)(*tmp1);for (int i = 0; i < 3; i++) {printf("vptr[%d] : %p\n", i, vptr1[i]);}Func a = (Func)vptr1[0];Func b = (Func)vptr1[1];Func c = (Func)vptr1[2];a();b();c();Derive* p = new Derive;long* tmp = (long*)p;long* vptr = (long*)(*tmp);cout << "---------Derive------------" << endl;for (int i = 0; i < 3; i++) {printf("vptr[%d] : %p\n", i, vptr[i]);}Func d = (Func)vptr[0];Func e = (Func)vptr[1];Func f = (Func)vptr[2];d();e();f();return 0;
}
image-20210525231248387

多重继承的虚函数表:

虚函数的引入其实就是为了实现多态(对于多态看到了一篇很不错的博客:传送门),现在来研究一下多重继承的虚函数表是什么样的,首先我们先来看一下简单的一般继承的代码:

class Base1 {
public:virtual void A() { cout << "Base1 A()" << endl; }virtual void B() { cout << "Base1 B()" << endl; }virtual void C() { cout << "Base1 C()" << endl; }
};class Derive : public Base1{
public:virtual void MyA() { cout << "Derive MyA()" << endl; }
};

image-20210525232550203

那么我们现在在Derive中再添加一个虚函数,让它覆盖基类中的虚函数,代码如下:

class Base1 {
public:virtual void A() { cout << "Base1 A()" << endl; }virtual void B() { cout << "Base1 B()" << endl; }virtual void C() { cout << "Base1 C()" << endl; }
};class Derive : public Base1{
public:virtual void MyA() { cout << "Derive MyA()" << endl; }virtual void B() { cout << "Derive B()" << endl; }
};

image-20210525233019931

这个是单继承的情况,然后我们看看多重继承,也就是Derive类继承两个基类,先看一下代码:

class Base1 {
public:virtual void A() { cout << "Base1 A()" << endl; }virtual void B() { cout << "Base1 B()" << endl; }virtual void C() { cout << "Base1 C()" << endl; }
};class Base2 {
public:virtual void D() { cout << "Base2 D()" << endl; }virtual void E() { cout << "Base2 E()" << endl; }
};class Derive : public Base1, public Base2{
public:virtual void A() { cout << "Derive A()" << endl; }           // 覆盖Base1::A()virtual void D() { cout << "Derive D()" << endl; }           // 覆盖Base2::D()virtual void MyA() { cout << "Derive MyA()" << endl; }
};

首先我们明确一个概念,对于多重继承的派生类来说,它含有多个虚函数指针,对于上述代码而言,Derive含有两个虚函数指针,所以它不是只有一个虚函数表,然后把所有的虚函数都塞到这一个表中,为了印证这一点,我们下面会印证这一点,首先我们先来看看这个多重继承的图示:

image-20210526132618945

#include <iostream>
#include <stdio.h>
using namespace std;class Base1 {
public:virtual void A() { cout << "Base1 A()" << endl; }virtual void B() { cout << "Base1 B()" << endl; }virtual void C() { cout << "Base1 C()" << endl; }
};class Base2 {
public:virtual void D() { cout << "Base2 D()" << endl; }virtual void E() { cout << "Base2 E()" << endl; }
};class Derive : public Base1, public Base2 {
public:virtual void A() { cout << "Derive A()" << endl; }           // 覆盖Base1::A()virtual void D() { cout << "Derive D()" << endl; }           // 覆盖Base2::D()virtual void MyA() { cout << "Derive MyA()" << endl; }
};int main()
{typedef void (*Func)();Derive d;Base1& b1 = d;Base2& b2 = d;cout << "Derive对象所占的内存大小为:" << sizeof(d) << endl;cout << "\n---------第一个虚函数表-------------" << endl;long* tmp1 = (long*)&d;              // 获取第一个虚函数表的指针long* vptr1 = (long*)(*tmp1);         // 获取虚函数表Func x1 = (Func)vptr1[0];Func x2 = (Func)vptr1[1];Func x3 = (Func)vptr1[2];Func x4 = (Func)vptr1[3];x1(); x2(); x3(); x4();cout << "\n---------第二个虚函数表-------------" << endl;long* tmp2 = tmp1 + 1;               // 获取第二个虚函数表指针 相当于跳过4个字节long* vptr2 = (long*)(*tmp2);Func y1 = (Func)vptr2[0];Func y2 = (Func)vptr2[1];y1(); y2();return 0;
}

image-20210526132802798

因为在包含一个虚函数表的时候,含有一个虚函数表指针,所占用的大小为4个字节,那么这里输出了8个字节,就说明Derive对象含有两个虚函数表指针。然后我们通过获取到了这两个虚函数表,并调用其对应的虚函数,可以发现输出的结果和上面的示例图是相同的,因此就证明了上述所说的结论是正确的。

1. 每一个基类都会有自己的虚函数表,派生类的虚函数表的数量根据继承的基类的数量来定。

2. 派生类的虚函数表的顺序,和继承时的顺序相同。

3. 派生类自己的虚函数放在第一个虚函数表的后面,顺序也是和定义时顺序相同。

4. 对于派生类如果要覆盖父类中的虚函数,那么会在虚函数表中代替其位置。

虚函数指针和虚函数表的创建时机:

​ 对于虚函数表来说,在编译的过程中编译器就为含有虚函数的类创建了虚函数表,并且编译器会在构造函数中插入一段代码,这段代码用来给虚函数指针赋值。因此虚函数表是在编译的过程中创建

​ 对于虚函数指针来说,由于虚函数指针是基于对象的,所以对象在实例化的时候,虚函数指针就会创建,所以是在运行时创建。由于在实例化对象的时候会调用到构造函数,所以就会执行虚函数指针的赋值代码,从而将虚函数表的地址赋值给虚函数指针。

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.mzph.cn/news/408191.shtml

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈email:809451989@qq.com,一经查实,立即删除!

相关文章

Proxy Pattern using C# (转载)

Proxy Pattern&#xff08;代理模式&#xff09;属于Structural Pattern&#xff08;结构型模式&#xff09;&#xff0c;Proxy Pattern-为Client真正要调用的对象提供一个代理&#xff08;Surrogate or placeholder&#xff09;&#xff0c;来控制Client对该对象的访问。 1. U…

BlackBerry 应用程序开发者指南 第一卷:基础--第5章 支持的媒体内容(Media Content)...

作者:Confach 发表于April 23,2006 15:02 pm版权信息:可以任意转载, 转载时请务必以超链接形式标明文章原始出处 和作者信息.http://www.cnblogs.com/confach/articles/387902.html5第5章 支持的媒体内容&#xff08;Media Content&#xff09;PME内容 播放媒体内容 监听媒体内…

Qt 入门 ---- 如何在程序窗口显示图片?

步骤&#xff1a; 1. 选择资源&#xff08;准备图片&#xff09; 2. 加载资源&#xff08;导入图片&#xff09; 3. 使用资源&#xff08;显示图片&#xff09; 具体操作流程&#xff1a; ① 从网上寻找合适的图片素材&#xff0c;下载到本地&#xff0c;在项目根目录下创建一个…

Enterprise Library 2.0 技巧(3):记录ASP.NET站点中未处理的异常

这篇文章不能算是Enterprise Library 2.0的一个技巧&#xff0c;只是Logging Application Block的一个简单应用而已&#xff0c;在这里我们使用Logging Application Block来记录一个ASP.NET 2.0站点中未处理的异常到数据库中&#xff0c;当然你也可以记录到文本文件中&#xff…

C++自定义对象如何支持Range-based循环语法

自定义对象如何支持Range-based循环语法 至少实现以下两种语法: //返回第一个迭代子的位置 Iterator begin() //返回最后一个迭代子的下一个位置 Iterator end()迭代子需要支持如下三种方法: operator(自增)operator! (判不等)operator* (解引用) #include <iostream>…

SharePoint 2013 本地开发解决方案以及程调试

SharePoint 2013 本地开发解决方案以及程调试 在SharePoint开发中&#xff0c;我们需要在部署有SharePoint环境的服务器中开发&#xff0c;这是一件让人很苦恼的事情&#xff0c;毕竟不能一个项目多人开发配备多台服务器&#xff0c;这就需要本地开发。 本来自己以为SharePoint…

Linux与C++11多线程编程(学习笔记)

多线程编程与资源同步 在Windows下,主线程退出后,子线程也会被关闭; 在Linux下,主线程退出后,系统不会关闭子线程,这样就产生了僵尸进程 3.2.1创建线程 Linux 线程的创建 #include <unistd.h> #include <stdio.h> #include <pthread.h> void* threadfunc(…

TCP网络编程的基本流程

TCP网络编程的基本流程 对于服务端,通常为以下流程: 调用socket函数创建socket调用bind函数将socket绑定到某个IP和端口上调用listen开始监听当有客户端请求连接上来时,调用accept函数接受连接,产生一个新的socket基于新产生的socket调用send或recv函数,开始与客户端进行数据…

Linux select函数用法和原理

select函数的用法和原理 Linux上的select函数 select函数用于检测一组socket中是否有事件就绪.这里的事件为以下三类: 读事件就绪 在socket内核中,接收缓冲区中的字节数大于或者等于低水位标记SO_RCVLOWAT,此时调用rec或read函数可以无阻塞的读取该文件描述符,并且返回值大于…

常见日期方法荟萃

一.如何获得当月有多少天 intmSystem.DateTime.DaysInMonth(System.DateTime.Now.Year,System.DateTime.Now.Month);二.日期型格式处理通用方法1.在webconfig中配置如下<add key"ShortDatePattern"value"MM-dd-yyyy"/><add key"LongDatePatt…