-
步骤一,先表述虚函数表的3个特性来做引子:
1, 单继承时,虚函数表指针通常存储在类对象“内存布局”的最前面。
2,虚函数表实质上是一个“函数指针”的数组,
该数组最后一个元素必然为0。(很多博客上都说虚函数表的最后一个元素是0,但我自己用vs2010做的实验有时候不是0)。
3,一个有虚函数(
无论是继承得到的虚函数还是自身有的
)的类,该类的所有对象,都共用一份虚函数表。
每个对象有一套(这里用套而不用个,是因为多重继承时,可能有多个指针组成的一套
)“虚函数表指针”,指向该虚函数表。
-
步骤二,下面来证明上面几个特性,并推导出类对象的内存布局。
//VC++ 32位编译器下
#include "stdafx.h"
#include <stdio.h>
#include <iostream>
using namespace std;
typedef void(*FUNC)();
class A{
public:
virtual void func(){
cout << " A::func" << endl;
}
virtual void funcA(){
cout << " A::funcA" << endl;
}
public:
int a;
};
class B:public A{
public:
virtual void func(){
cout << " B::func" << endl;
}
virtual void funcB(){
cout << " B::funcB" << endl;
}
public:
int b;
};
int main()
{
B b1;
B b2;
printf("\n b1对象的首地址里面存放的虚函数表的指针是:0x%x\n", (*(int*)&b1));
((FUNC)( *(int*)*((int*)&b1) ))();
((FUNC)*((int*)*((int*)&b1) + 1))();
printf(" b2对象的首地址里面存放的虚函数表的指针是:0x%x\n", (*(int*)&b2));
system("pause");
return 0;
}
输出结果:由输出结果可以看出,程序正确调用了两个虚函数(先不管调用的是什么虚函数),所以找到的虚函数表的指针是正确的。步骤一中的1得到证实。又因为 b1 和 b2 虚函数表的指针值是相同的,所以步骤一中的3也得到了证实。
-
步骤三,再来看一个单继承的例子
//VC++ 32位编译器下:
#include "stdafx.h"
#include <iostream>
using namespace std;
//单继承下虚函数表:是如何组织的
class A{
public:
virtual void func(){
cout << "A::funccommon" << endl;
}
virtual void funcA(){
cout << "A::funcA" << endl;
}
};
class B:public A{
public:
virtual void func(){
cout << "B::funccommon" << endl;
}
virtual void funcB(){
cout << "B::funcB" << endl;
}
};
class C:public A{
public:
virtual void func(){
cout << "C::funccommon" << endl;
}
virtual void funcC(){
cout << "C::funcC" << endl;
}
};
typedef void (*FUNC)();
int main()
{
A a;
B b;
C c;
cout << "A::虚表:" << endl;
((FUNC)(*(int*)(*(int*)(&a))))();
((FUNC)(*((int*)(*(int*)(&a)) + 1)))();
cout << "-------------------------------------" << endl;
cout << "B::虚表:" << endl;
((FUNC)(*(int *)(*(int*)(&b))))();
((FUNC)(*((int*)(*(int*)(&b)) + 1)))();
((FUNC)(*((int*)(*(int*)(&b)) + 2)))();
cout << "-------------------------------------" << endl;
cout << "C::虚表:" << endl;
((FUNC)(*(int *)(*(int*)(&c))))();
((FUNC)(*((int*)(*(int*)(&c)) + 1)))();
((FUNC)(*((int*)(*(int*)(&c)) + 2)))();
system("pause");
return 0;
}
输出结果:在分析输出结果之前,先看一下这句代码是什么意思?
( (FUNC) ( *(int*) (*(int*)(&a)) ) )();
(*(int*)(&a))的意思是,从对象 a 的起始地址所指向的那个字节的位置算起,取4个字节的一个整形值。我们知道,在VC++ 32位编译器下,指针和 int 型一样,也是占4个字节。
所以实际上,取出来的这个整形值,也可以看作是一个地址(也称指针,实际上该指针就是对象a指向虚函数表的指针)。
( *(int*) (*(int*)(&a)) )的意思是,将上面取出的指针,强制转换为 int* 的指针,然后取出该指针所指的整形值(同样,也可以看作是一个地址),该整形值事实上是一个函数指针。
由以上分析可以对代码进行解释:
我们再来看刚才的输出结果,我们对虚函数表,按照由数组首地址,计算偏移获取到数组元素的做法,获取了A, B, C三个类里的虚函数的指针,并且都调用成功了。
由此步骤一中的2也得到了证实。
-
步骤四,如果派生类的虚函数和基类的虚函数相同,即派生类的虚函数“覆盖”了基类的虚函数,则在派生类的虚函数表中,只有派生类的那个虚函数。如果是派生类新增的虚函数,则将该虚函数追加到派生类虚函数表的末尾。如下图,是类B的虚函数表的产生过程:
-
步骤五,虚函数表的生成的时间
这里说明下一个问题,我用 VS2010 的 Debug 模式来调试 步骤三 里面的代码,然后在监视窗口里查看变量b, c的虚函数表,发现只能查看到 从基类继承下来的 和 派生类覆盖的 虚函数,不能看到派生类自己追加的虚函数。
而且对于对 虚函数表 没有深刻理解的人来说,VS2010 的显示方式让人容易误解,认为存储的是基类的虚函数表的指针。如下图:
VS运行起来,可以打印上图 b 和 c 的_vfptr[2],但是不明白为什么在调试器里不能查看到。
现在在 linux 下用 gdb 来查看:
注:上图中,因为我的g++编译器是64位的,所以把 int 改成了 long long 类型。因为在g++ 64位编译器下,指针是占 8个字节的,long long 类型也是占8个字节的。当然直接用 long 也行。
程序构建(Build)的四个过程(预编译、编译、汇编和链接)
虚函数表应该是在编译期确定的,原因如下:1)预编译器主要处理那些源代码文件中的以“#”开始的预编译指令,如“#include”、“#define”。很明显这个过程可以排除。
2)汇编器是将编译器生成的汇编代码转变成机器可以执行的指令,每一个汇编语句几乎都对应一条机器指令。汇编过程相对于编译期来说比较简单,没有复杂的语法,也没有语义,也不需要做指令优化,只是根据汇编指令和机器指令的对照表一一翻译就行了。所以,汇编期也是可以排除的。
3)链接器(现只考虑静态链接)是将汇编器生成的目标文件(和库)链接成一个可执行文件,本质上做的是重定位(Relocation)的工作,详细可参考《程序员的自我修养》2.3、2.4节。很明显链接期也是可以排除的。
4)编译器要做的事情就比较多了,包括词法分析、语法分析、语义分析及优化代码等,是整个程序构建的核心。所以,排除了预编译期、汇编期、链接期及考虑到编译期所做的事情,虚函数表应该是在编译期建立的。
为什么不是在运行时确定的呢?
C++是编译型语言,当然是在编译阶段把能够做的工作都做完,执行起来效率更高。像多态那种因为用户行为会影响执行路径的,才不得不在执行阶段确定。
-
步骤五,虚函数表存放在进程(在磁盘上称 ”可执行文件”,在内存中就称 “进程”)的哪个区?
用readelf命令查看,这个以后再回来学习并补充。开始因为不知道还有readelf这种命令,也不知道有elf文件这种格式,我愚蠢地去学习了一下汇编,想用反汇编的方法看进程在内存中分布,搞得花了一天的时间来折腾,还没有好的结果。再写的时候参考一下 https://blog.csdn.net/chenlycly/article/details/53377942 这篇博文
。
借用别人博客的一句话先告诉结果:vtable在Linux/Unix中存放在可执行文件的只读数据段中(rodata)
,而微软的编译器将虚函数表存放在常量段
。
-
步骤六,多重继承时 ,派生类对象的内存布局
多重继承的概念:网上很多人的博客对多继承
和多重继承
两个概念有不同的解释,搞得很混乱。尤其是多重继承
,有些博客错得离谱,说类C继承自类B,类B继承自类A,这样叫多重继承。对多继承
歧义的倒比较少。这里我统一一下概念,根据C++ Primer中文版(第4版)
的说法:
多重继承是从多于一个直接基类派生类的能力
,多重继承的派生类继承其所有父类的属性。
多继承 与 多重继承实际上是一个概念。
//多继承条件下的虚函数表
#include "stdafx.h"
#include <iostream>
using namespace std;
#include<iostream>
using namespace std;
class A
{
public:
virtual void fun1()
{
printf("A::virtual void fun(int n)\n");
}
int _a;
};
class B
{
public:
virtual void fun2()
{
printf("B::virtual void fun(int n)\n");
}
int _b;
};
class C:public A,public B
{
public:
int _c;
};
int main()
{
A a;
B b;
C c;
a._a = 1;
b._b = 2;
c._a = 3;
c._b = 4;
c._c = 5;
printf("%p\n", &a);
printf("%p\n", &b);
printf("%p\n", &c);
return 0;
}
内存窗口分析:
-
步骤七,虚继承的作用
虚继承是解决C++多重继承问题的一种手段,从不同途径继承来的同一基类,会在子类中存在多份拷贝。
这将存在两个问题:
其一,浪费存储空间;
第二,存在二义性问题,通常可以将派生类对象的地址赋值给基类对象,实现的具体方式是,将基类指针指向继承类(继承类有基类的拷贝)中的基类对象的地址,但是多重继承可能存在一个基类的多份拷贝,这就出现了二义性。
虚继承是为了解决上述两个问题而产生的。
这个比较复杂,用一篇博客里的说法就是,其复杂度远远大于它的使用价值,不想花太多时间研究,仅仅知道其用法就行了。
如果很想知道,可以参考一下https://blog.csdn.net/zhourong0511/article/details/79950847这篇博客的最后一张图,那里讲的菱形继承里有虚继承的内容。
参考了以下两篇博客:
1,https://blog.csdn.net/zongyinhu/article/details/51276806?tdsourcetag=s_pcqq_aiomsg。发现作者讲得很透彻,为了我能完全弄懂并记住虚函数表的有关问题,现在用自己的话整理出来,并发布。
2,https://blog.csdn.net/zhourong0511/article/details/79950847使用的内存表示方法非常好,让我完全看懂了,不过博客中有少量错误和歧义的内容。