【C++ Priemr | 15】虚函数表剖析(二)

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

下面,再让我们来看看多重继承中的情况,假设有下面这样一个类的继承关系。注意:子类并没有覆盖父类的函数。

测试代码:

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 f1() { cout << "Derive::f1" << endl; } //虚函数定义virtual void g1() { cout << "Derive::g1" << endl; }
};

对于子类实例中的虚函数表,是下面这个样子:

我们可以看到:

  • 每个父类都有自己的虚表。
  • 子类的成员函数被放到了第一个父类的表中。(所谓的第一个父类是按照声明顺序来判断的)

这样做就是为了解决不同的父类类型的指针指向同一个子类实例,而能够调用到实际的函数。

 

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

下面我们再来看看,如果发生虚函数覆盖的情况。

测试代码: 

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; }
};

下图中,我们在子类中覆盖了父类的f()函数。

下面是对于子类实例中的虚函数表的图:

我们可以看见,三个父类虚函数表中的f()的位置被替换成了子类的函数指针。这样,我们就可以任一静态类型的父类来指向子类,并调用子类的f()了。如:

Derive d;
Base1 *b1 = &d;
Base2 *b2 = &d;
Base3 *b3 = &d;
b1->f(); //Derive::f()
b2->f(); //Derive::f()
b3->f(); //Derive::f()b1->g(); //Base1::g()
b2->g(); //Base2::g()
b3->g(); //Base3::g()

安全性

每次写C++的文章,总免不了要批判一下C++。这篇文章也不例外。通过上面的讲述,相信我们对虚函数表有一个比较细致的了解了。水可载舟,亦可覆舟。下面,让我们来看看我们可以用虚函数表来干点什么坏事吧。

1. 通过父类型的指针访问子类自己的虚函数

我们知道,子类没有重载父类的虚函数是一件毫无意义的事情。因为多态也是要基于函数重载的。虽然在上面的图中我们可以看到Base1的虚表中有Derive的虚函数,但我们根本不可能使用下面的语句来调用子类的自有虚函数:

Base1 *b1 = new Derive();
b1->g1();  //编译出错

任何妄图使用父类指针想调用子类中的未覆盖父类的成员函数的行为都会被编译器视为非法,所以,这样的程序根本无法编译通过。但在运行时,我们可以通过指针的方式访问虚函数表来达到违反C++语义的行为。(关于这方面的尝试,通过阅读后面附录的代码,相信你可以做到这一点)

 

2. 访问non-public的虚函数

另外,如果父类的虚函数是private或是protected的,但这些非public的虚函数同样会存在于虚函数表中,所以,我们同样可以使用访问虚函数表的方式来访问这些non-public的虚函数,这是很容易做到的。如

#include<iostream>
using namespace std;class Base {
private:virtual void f() { cout << "Base::f" << endl; }
};class Derive : public Base {};typedef void(*Fun)(void);int main() 
{Derive d;Fun  pFun = (Fun)*((int*)*(int*)(&d) + 0);pFun();
}

 输出结果:

 

三、多重继承

下面,再让我们来看看多重继承中的情况,假设有下面这样一个类的继承关系。

注意:子类只overwrite了父类的f()函数,而还有一个是自己的函数(我们这样做的目的是为了用g1()作为一个标记来标明子类的虚函数表)。而且每个类中都有一个自己的成员变量:

们的类继承的源代码如下所示:父类的成员初始为10,20,30,子类的为100

#include<iostream>
using namespace std;class Base1 {
public:int ibase1;Base1() :ibase1(10) {}virtual void f() { cout << "Base1::f()" << endl; }virtual void g() { cout << "Base1::g()" << endl; }virtual void h() { cout << "Base1::h()" << endl; }};class Base2 {
public:int ibase2;Base2() :ibase2(20) {}virtual void f() { cout << "Base2::f()" << endl; }virtual void g() { cout << "Base2::g()" << endl; }virtual void h() { cout << "Base2::h()" << endl; }
};class Base3 {
public:int ibase3;Base3() :ibase3(30) {}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:int iderive;Derive() :iderive(100) {}virtual void f() { cout << "Derive::f()" << endl; }virtual void g1() { cout << "Derive::g1()" << endl; }
};int main()
{typedef void(*Fun)(void);Derive d;int** pVtab = (int**)&d;cout << "[0] Base1::_vptr->" << endl;Fun pFun = (Fun)pVtab[0][0];cout << "     [0] ";pFun();pFun = (Fun)pVtab[0][1];cout << "     [1] "; pFun();pFun = (Fun)pVtab[0][2];cout << "     [2] "; pFun();pFun = (Fun)pVtab[0][3];cout << "     [3] "; pFun();pFun = (Fun)pVtab[0][4];cout << "     [4] "; cout << pFun << endl;cout << "[1] Base1.ibase1 = " << (int)pVtab[1] << endl;int s = sizeof(Base1) / 4;cout << "[" << s << "] Base2::_vptr->" << endl;pFun = (Fun)pVtab[s][0];cout << "     [0] "; pFun();pFun = (Fun)pVtab[s][1];cout << "     [1] "; pFun();pFun = (Fun)pVtab[s][2];cout << "     [2] "; pFun();pFun = (Fun)pVtab[s][3];cout << "     [3] ";cout << pFun << endl;cout << "[" << s + 1 << "] Base2.ibase2 = " << (int)pVtab[s + 1] << endl;s = s + sizeof(Base2) / 4;cout << "[" << s << "] Base3::_vptr->" << endl;pFun = (Fun)pVtab[s][0];cout << "     [0] "; pFun();pFun = (Fun)pVtab[s][1];cout << "     [1] "; pFun();pFun = (Fun)pVtab[s][2];cout << "     [2] "; pFun();pFun = (Fun)pVtab[s][3];cout << "     [3] ";cout << pFun << endl;s++;cout << "[" << s << "] Base3.ibase3 = " << (int)pVtab[s] << endl;s++;cout << "[" << s << "] Derive.iderive = " << (int)pVtab[s] << endl;
}

输出结果:

使用图片表示是下面这个样子:

我们可以看到:

  • 每个父类都有自己的虚表。
  • 子类的成员函数被放到了第一个父类的表中。
  • 内存布局中,其父类布局依次按声明顺序排列。
  • 每个父类的虚表中的f()函数都被overwrite成了子类的f()。这样做就是为了解决不同的父类类型的指针指向同一个子类实例,而能够调用到实际的函数。

 

四、重复继承

面我们再来看看,发生重复继承的情况。所谓重复继承,也就是某个基类被间接地重复继承了多次。

下图是一个继承图,我们重载了父类的f()函数。其类继承的源代码如下所示。其中,每个类都有两个变量,一个是整形(4字节),一个是字符(1字节),而且还有自己的虚函数,自己overwrite父类的虚函数。如子类D中,f()覆盖了超类的函数, f1() 和f2() 覆盖了其父类的虚函数,Df()为自己的虚函数。

测试代码: 

#include<iostream>
using namespace std;class B {
public:int ib;char cb;
public:B() : ib(0), cb('B') {}virtual void f() { cout << "B::f()" << endl; }virtual void Bf() { cout << "B::Bf()" << endl; }
};class B1 : public B {
public:int ib1;char cb1;
public:B1() : ib1(11), cb1('1') {}virtual void f() { cout << "B1::f()" << endl; }virtual void f1() { cout << "B1::f1()" << endl; }virtual void Bf1() { cout << "B1::Bf1()" << endl; }
};class B2 : public B {
public:int ib2;char cb2;
public:B2() :ib2(12), cb2('2') {}virtual void f() { cout << "B2::f()" << endl; }virtual void f2() { cout << "B2::f2()" << endl; }virtual void Bf2() { cout << "B2::Bf2()" << endl; }};class D : public B1, public B2 {
public:int id;char cd;
public:D() : id(100), cd('D') {}virtual void f() { cout << "D::f()" << endl; }virtual void f1() { cout << "D::f1()" << endl; }virtual void f2() { cout << "D::f2()" << endl; }virtual void Df() { cout << "D::Df()" << endl; }};int main()
{typedef void(*Fun)(void);int** pVtab = NULL;Fun pFun = NULL;D d;pVtab = (int**)&d;cout << "[0] D::B1::_vptr->" << endl;pFun = (Fun)pVtab[0][0];cout << "     [0] ";    pFun();pFun = (Fun)pVtab[0][1];cout << "     [1] ";    pFun();pFun = (Fun)pVtab[0][2];cout << "     [2] ";    pFun();pFun = (Fun)pVtab[0][3];cout << "     [3] ";    pFun();pFun = (Fun)pVtab[0][4];cout << "     [4] ";    pFun();pFun = (Fun)pVtab[0][5];cout << "     [5] 0x" << pFun << endl;cout << "[1] B::ib = " << (int)pVtab[1] << endl;cout << "[2] B::cb = " << (char)pVtab[2] << endl;cout << "[3] B1::ib1 = " << (int)pVtab[3] << endl;cout << "[4] B1::cb1 = " << (char)pVtab[4] << endl;cout << "[5] D::B2::_vptr->" << endl;pFun = (Fun)pVtab[5][0];cout << "     [0] ";    pFun();pFun = (Fun)pVtab[5][1];cout << "     [1] ";    pFun();pFun = (Fun)pVtab[5][2];cout << "     [2] ";    pFun();pFun = (Fun)pVtab[5][3];cout << "     [3] ";    pFun();pFun = (Fun)pVtab[5][4];cout << "     [4] 0x" << pFun << endl;cout << "[6] B::ib = " << (int)pVtab[6] << endl;cout << "[7] B::cb = " << (char)pVtab[7] << endl;cout << "[8] B2::ib2 = " << (int)pVtab[8] << endl;cout << "[9] B2::cb2 = " << (char)pVtab[9] << endl;cout << "[10] D::id = " << (int)pVtab[10] << endl;cout << "[11] D::cd = " << (char)pVtab[11] << endl;
}

输出结果:

下面是对于子类实例中的虚函数表的图:(第一份图为原作者的图,第二幅图为修改的图)

 

我们可以看见,最顶端的父类B其成员变量存在于B1和B2中,并被D给继承下去了。而在D中,其有B1和B2的实例,于是B的成员在D的实例中存在两份,一份是B1继承而来的,另一份是B2继承而来的。所以,如果我们使用以下语句,则会产生二义性编译错误:

1

2

3

4

D d;

d.ib = 0; //二义性错误

d.B1::ib = 1; //正确

d.B2::ib = 2; //正确

注意,上面例程中的最后两条语句存取的是两个变量。虽然我们消除了二义性的编译错误,但B类在D中还是有两个实例,这种继承造成了数据的重复,我们叫这种继承为重复继承。重复的基类数据成员可能并不是我们想要的。所以,C++引入了虚基类的概念。

 

参考资料:

  • C++ 虚函数表解析 陈皓著
  • C++ 对象的内存布局 陈皓著

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

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

相关文章

1074. Reversing Linked List (25)

Given a constant K and a singly linked list L, you are supposed to reverse the links of every K elements on L. For example, given L being 1→2→3→4→5→6, if K 3, then you must output 3→2→1→6→5→4; if K 4, you must output 4→3→2→1→5→6. Input Spe…

【Leetcode | 47】 222. 完全二叉树的节点个数

给出一个完全二叉树&#xff0c;求出该树的节点个数。 说明&#xff1a; 完全二叉树的定义如下&#xff1a;在完全二叉树中&#xff0c;除了最底层节点可能没填满外&#xff0c;其余每层节点数都达到最大值&#xff0c;并且最下面一层的节点都集中在该层最左边的若干位置。若最…

makefile中的两个函数(wildcard和patsubst)

(1) wildcard函数 作用是查找指定目录下指定类型的文件&#xff0c;并最终返回一个环境变量&#xff0c;需要用$取值赋值给另一个环境变量&#xff01;该函数只有一个参数&#xff0c;如取出当前目录下的所有.c文件&#xff0c;并赋值给allc普通变量&#xff1a; allc$(wildc…

231. 2的幂

给定一个整数&#xff0c;编写一个函数来判断它是否是 2 的幂次方。 示例 1: 输入: 1 输出: true 解释: 20 1 示例 2: 输入: 16 输出: true 解释: 24 16 示例 3: 输入: 218 输出: false 解法一&#xff1a; class Solution { public:bool isPowerOfTwo(int n) {return(n >…

C库函数

Linux的系统I/O函数&#xff08;read、write、open、close和 lseek等&#xff09;与C语言的C库函数&#xff08;libc.so库文件中&#xff09;都是相对应的&#xff0c;它们都是动态库函数。如下图所示&#xff0c;C库函数有fopen、fclose、fwrite、fread和fseek等。这些C库函数…

【Leetcode | 48】226. 翻转二叉树

翻转一棵二叉树。 示例&#xff1a; 输入&#xff1a; 4 / \ 2 7 / \ / \ 1 3 6 9 输出&#xff1a; 4 / \ 7 2 / \ / \ 9 6 3 1 备注: 这个问题是受到 Max Howell 的 原问题 启发的 &#xff1a; 谷歌&#xff1a;我们90&#xff05;的…

C库函数与Linux系统函数之间的关系

由上小节知道&#xff0c;C库函数是借助FILE类型的结构体来对文件进行操作的&#xff0c;其本身只是在用户空间&#xff08;I/O缓冲区&#xff09;进行读写操作&#xff0c;而数据在内核与用户空间之间的传递、以及将内核与I/O设备之间的数据传递都是该C库函数进行一系列的系统…

【第十六章】模板实参推断

二、模板显式推断 在C中&#xff0c;若函数模板返回类型需要用户指定&#xff0c;那么在定义函数模板时&#xff0c;模板参数的顺序是很重要的&#xff0c;如下代码&#xff1a; template <typename T1, typename T2, typename T3> //模板一 T1 sum(T2 a, T3 b) {retu…

open函数和errno全局变量

&#xff08;1&#xff09;open函数 man man 查看man文档的首页 其中DESCRIPTION部分描述了man文档的每一章的章节内容 第2章System calls为系统调用&#xff0c;即Liunx系统函数。 man 2 open 查看第二章的open函数的详细帮助文件。 open函数用于打开一个已经的文件或者创…

open函数和close函数的使用

学习几个常用的Linux系统I/O函数&#xff1a;open、close、write、read和lseek。注意&#xff0c;系统调用函数必须都考虑返回值。 &#xff08;1&#xff09;open函数的使用 首先&#xff0c;需要包含三个头文件&#xff1a;<sys/types.h> <sys/stat.h> <…

【Leetcode | 9】217. 存在重复元素

解题代码&#xff1a; bool containsDuplicate(vector<int>& nums) {return nums.size() > set<int>(nums.begin(), nums.end()).size(); }

全缓冲、行缓冲和无缓冲

这里的缓冲是指的是用户空间的I/O缓冲区&#xff0c;不是内核缓冲。 无缓冲&#xff1a;用户层不提供缓冲&#xff0c;数据流直接到内核缓冲&#xff0c;再到磁盘等外设上。标准错误输出&#xff08;2&#xff09;通常是无缓存的&#xff0c;因为它必须尽快输出&#xff0c;且…

【Leetcode】1. 两数之和

给定一个整数数组 nums 和一个目标值 target&#xff0c;请你在该数组中找出和为目标值的那 两个 整数&#xff0c;并返回他们的数组下标。 你可以假设每种输入只会对应一个答案。但是&#xff0c;你不能重复利用这个数组中同样的元素。 示例: 给定 nums [2, 7, 11, 15], targ…

read和write函数的使用

都需要包含头文件&#xff1a; <unistd.h> read系统函数从打开的设备或文件中读取数据&#xff0c;即将数据从外设上经过内核读到用户空间&#xff1b;write系统函数相反&#xff0c;向打开的设备或文件中写入数据&#xff0c;即将数据从用户空间&#xff08;I/O缓冲&am…

1091. Acute Stroke (30)

One important factor to identify acute stroke (急性脑卒中) is the volume of the stroke core. Given the results of image analysis in which the core regions are identified in each MRI slice, your job is to calculate the volume of the stroke core. Input Speci…

lseek函数的使用

需要包含头文件&#xff1a;<sys/types.h> <unistd.h> off_t lseek(int fd, off_t offset, int whence)&#xff1b; 函数原型 函数功能&#xff1a;移动文件读写指针&#xff1b;获取文件长度&#xff1b;拓展文件空间。 在使用该函数之前需要将文件打开&…

19. 删除链表的倒数第N个节点

给定一个链表&#xff0c;删除链表的倒数第 n 个节点&#xff0c;并且返回链表的头结点。 示例&#xff1a; 给定一个链表: 1->2->3->4->5, 和 n 2. 当删除了倒数第二个节点后&#xff0c;链表变为 1->2->3->5. 说明&#xff1a; 给定的 n 保证是有效的。…

文件操作相关的系统函数

重点学习&#xff1a;stat&#xff08;fstat、lstat 获取文件属性&#xff09;、access&#xff08;测试指定文件是否拥有某种权限&#xff09;、chmod&#xff08;改变文件的权限&#xff09;、chown&#xff08;改变文件的所属主和所属组&#xff09;、truncate&#xff08;截…

stat函数(stat、fstat、lstat)

#include <sys/types.h> #include <sys/stat.h> #include <unistd.h> //需包含头文件 有如下三个函数的函数原型&#xff1a; int stat(const char *path, struct stat *buf); 第一个形参&#xff1a;指出文件&#xff08;文件路径&#xff09;&…

1062. Talent and Virtue (25)

About 900 years ago, a Chinese philosopher Sima Guang wrote a history book in which he talked about peoples talent and virtue. According to his theory, a man being outstanding in both talent and virtue must be a "sage&#xff08;圣人&#xff09;"…