这篇我们延续上次的虚函数分析,来研究下多态的本质。
虚函数逆向分析的博客:C++逆向分析--虚函数(多态的前置)-CSDN博客
有了上篇虚函数的知识,我们在正向开发学习的时候知道,多态的发生需要存在继承关系,并且子类重写父类方法,父类需要重写的方法是虚函数。这么几点要求。我一开始接触多态是在学习Java的时候。当时的多态搞的我是一脸懵逼。不是很理解是怎么回事。但是在C++中一切的不理解我们都可以直接去逆向分析剖析他的原理。(Java其实也可以太蠢了不会)我相信C++的学懂了,Java学起来理解起来会更加得心应手。这是大佬告诉我的。
废话不多说,先来一段demo:
#include<iostream>
using namespace std;
class Base {
public:
virtual void func1() {
printf("这是父类的fun1\n");
}
virtual void func2() {
printf("这是父类的func2\n");
}
};
class Base_Son :public Base {
public:
virtual void func3() {
printf("这是子类的func3\n");
}
virtual void func4() {
printf("这是子类的fun4\n");
}
};
int main() {
Base_Son s1;
int i = 0;
for (i = 0; i < 4; i++) {
int fun_call= *((int*)*((int*)&s1)+i);
printf("调用函数地址=%p\n", fun_call);
_asm {
call fun_call;
}
}
return 0;
}
我先大致解析下这段代码,主要有两个类,一个父类,一个子类。分别定义了4个虚函数。子类继承父类。
现在我们直接利用虚函数表去调用这四个函数。观察下效果:
有了上篇虚函数的知识,相信这里很容易理解。我就不一一分析了。但是这个demo告诉我们一件事情:当子类继承父类的时候,如果没有和父类同名的函数,那就没有重写父类的函数,并且只要有定义虚函数,那么在虚函数表中就能找到对应的函数地址。否则是不会在虚函数表中有记录。这就是单继承无函数覆盖。
画个图理解下这个过程:
下面我们重写父类的方法,再次观察虚表的变化(我们只需要在原demo做一丢丢手脚让子类的方方法名和父类同名):
#include<iostream>
using namespace std;
class Base {
public:
virtual void func1() {
printf("这是父类的fun1\n");
}
virtual void func2() {
printf("这是父类的func2\n");
}
};
class Base_Son :public Base {
public:
virtual void func1() { //与父类方法同名
printf("这是子类的func3\n");
}
virtual void func2() { //与父类方法同名
printf("这是子类的fun4\n");
}
};
int main() {
Base_Son s1;
int i = 0;
for (i = 0; i < 2; i++) {
int fun_call= *((int*)*((int*)&s1)+i);
printf("调用的函数地址=0x%p\n", fun_call);
_asm {
call fun_call;
}
}
return 0;
}
现在我们再次运行程序观察效果:
神奇的事情发生了。(爸爸去哪了?)我们仅仅是改了子类的方法名,但是此时父类父类的方法没了,并且替换成了子类的方法。说明子类方法覆盖了父类的方法也就是发生了重写:
我们知道C++是支持多继承的。也就是一个子类可以继承多个父类。这就是很反人类的地方(一个儿子可以有几个爸爸)。那么底层又是如何实现的呢?demo如下:
#include<iostream>
using namespace std;
class Base {
public:
virtual void func1() {
printf("这是父类的func1\n");
}
virtual void func2() {
printf("这是父类的func2\n");
}
};
class Base_Son{
public:
virtual void func3() {
printf("这是子类的func3\n");
}
virtual void func4() {
printf("这是子类的func4\n");
}
};
class Base_Grandson:public Base,public Base_Son {
public:
virtual void func5() {
printf("这是孙子类的func5\n");
}
virtual void func6() {
printf("这是孙子类的func6\n");
}
};
int main() {
Base_Grandson g1;
printf("g1的大小为=%d", sizeof(g1));
return 0;
}
大概解释下这个代码,现在我们创建了三个类,一个父类,一个子类,一个孙子类。我们让孙子类同时继承父类和子类。此时我们猜测下用孙子类创建的对象大小是多大。(没有成员属性的情况下):
大小竟然是8。我们再用之前的方法去调用函数试试:
#include<iostream>
using namespace std;
class Base {
public:
virtual void func1() {
printf("这是父类的func1\n");
}
virtual void func2() {
printf("这是父类的func2\n");
}
};
class Base_Son{
public:
virtual void func3() {
printf("这是子类的func3\n");
}
virtual void func4() {
printf("这是子类的func4\n");
}
};
class Base_Grandson:public Base,public Base_Son {
public:
virtual void func5() {
printf("这是孙子类的func5\n");
}
virtual void func6() {
printf("这是孙子类的func6\n");
}
};
int main() {
Base_Grandson g1;
int i = 0;
for (i = 0; i < 6; i++) {
int fun_call= *((int*)*((int*)&g1)+i);
printf("调用的函数地址=0x%p\n", fun_call);
_asm {
call fun_call;
}
}
return 0;
}
输出结果如下:
我们惊讶的发现只调用了父类和孙子类的方法。这个代码是有bug的,因为虚函数表中只存在了4个函数地址,但是我们却再调用的时候遍历了6个因此程序是有问题的。但是我这样写只是为了验证这个虚函数表中确实只调用了4个方法。那我上面也继承了我的子类呀,为啥子类的方法没了。而且为啥孙子类的对象是8个字节。这就是我们要探究的问题:
先说结论:有多少个直接父类,就有多少张虚函数表。因此,我们的代码中有两个直接父类。所以在孙子类对象中存在两张虚函数表。
那我们在验证下:
#include<iostream>
using namespace std;
class Base {
public:
virtual void func1() {
printf("这是父类的func1\n");
}
virtual void func2() {
printf("这是父类的func2\n");
}
};
class Base_Son{
public:
virtual void func3() {
printf("这是子类的func3\n");
}
virtual void func4() {
printf("这是子类的func4\n");
}
};
class Base_Grandson:public Base,public Base_Son {
public:
virtual void func5() {
printf("这是孙子类的func5\n");
}
virtual void func6() {
printf("这是孙子类的func6\n");
}
};
int main() {
Base_Grandson g1;
int i = 0;
for (i = 0; i < 4; i++) {
int fun_call= *((int*)*((int*)&g1)+i);
printf("调用的函数地址=0x%p\n", fun_call);
_asm {
call fun_call;
}
}
printf("---------------------------------------------------\n");
for (i = 0; i < 4; i++) {
int fun_call2 = *((int*)*((int*)&g1+1) + i); //取第二张虚表
printf("调用的函数地址=0x%p\n", fun_call2);
_asm {
call fun_call2;
}
}
return 0;
}
运行结果为:
我们看到第二张虚函数表中,存放的是子类的函数地址。我们总结一下:
在多继承没有发生重写的情况下,第一张虚函数表中放的是第一个继承的父类的函数地址和孙子类的函数地址。而在第二张虚函数表中放的是第二个继承的子类的虚函数地址。也就是说除了第一个直接继承的父类,后面继承的父类的虚函数地址均会在其他表中。(假设有三个直接继承的父类,那么第三个父类的虚函数的地址会在第三张虚函数表中)图示如下:
#include<iostream>
using namespace std;
class Base {
public:
virtual void func1() {
printf("这是父类的func1\n");
}
virtual void func2() {
printf("这是父类的func2\n");
}
};
class Base_Son{
public:
virtual void func3() {
printf("这是子类的func3\n");
}
virtual void func4() {
printf("这是子类的func4\n");
}
};
class Base_Grandson:public Base,public Base_Son {
public:
virtual void func1() { //发生重写,重写父类的方法
printf("这是孙子类的func5\n");
}
virtual void func2() { //发生重写,重写父类的方法
printf("这是孙子类的func6\n");
}
};
int main() {
Base_Grandson g1;
int i = 0;
for (i = 0; i < 2; i++) {
int fun_call= *((int*)*((int*)&g1)+i);
printf("调用的函数地址=0x%p\n", fun_call);
_asm {
call fun_call;
}
}
printf("---------------------------------------------------\n");
for (i = 0; i < 4; i++) {
int fun_call2 = *((int*)*((int*)&g1+1) + i); //取第二张虚表
printf("调用的函数地址=0x%p\n", fun_call2);
_asm {
call fun_call2;
}
}
return 0;
}
这里我们只做了一点手脚,让孙子类重写父类的方法观察结果:
我们发现父类的方法被覆盖掉了,也就是一旦子类重写父类,父类的方法将不会被调用,也就没必要在虚函数表中留下函数地址。我们在试试重写子类的方法改改我们的demo:
#include<iostream>
using namespace std;
class Base {
public:
virtual void func1() {
printf("这是父类的func1\n");
}
virtual void func2() {
printf("这是父类的func2\n");
}
};
class Base_Son{
public:
virtual void func3() { //发生重写,重写父类的方法
printf("这是子类的func3\n");
}
virtual void func4() { //发生重写,重写父类的方法
printf("这是子类的func4\n");
}
};
class Base_Grandson:public Base,public Base_Son {
public:
virtual void func3() {
printf("这是孙子类的func5\n");
}
virtual void func4() {
printf("这是孙子类的func6\n");
}
};
int main() {
Base_Grandson g1;
int i = 0;
for (i = 0; i < 2; i++) {
int fun_call= *((int*)*((int*)&g1)+i);
printf("调用的函数地址=0x%p\n", fun_call);
_asm {
call fun_call;
}
}
printf("---------------------------------------------------\n");
for (i = 0; i < 4; i++) {
int fun_call2 = *((int*)*((int*)&g1+1) + i); //取第二张虚表
printf("调用的函数地址=0x%p\n", fun_call2);
_asm {
call fun_call2;
}
}
return 0;
}
这里我们让孙子类重写子类的方法,运行结果如下:
我们发现覆盖的是第二张虚函数表的地址。那么由上两个实验我们得出一个结论:
重写谁的方法将会覆盖谁的虚函数表的地址。
#include<iostream>
using namespace std;
class Base {
public:
virtual void func1() {
printf("这是父类的func1\n");
}
virtual void func2() {
printf("这是父类的func2\n");
}
};
class Base_Son :public Base{
public:
virtual void func3() {
printf("这是子类的func3\n");
}
virtual void func4() {
printf("这是子类的func4\n");
}
};
class Base_Grandson : public Base_Son {
public:
virtual void func5() {
printf("这是孙子类的func5\n");
}
virtual void func6() {
printf("这是孙子类的func6\n");
}
};
int main() {
Base_Grandson g1;
int i = 0;
for (i = 0; i < 6; i++) {
int fun_call= *((int*)*((int*)&g1)+i);
printf("调用的函数地址=0x%p\n", fun_call);
_asm {
call fun_call;
}
}
return 0;
}
多重继承就是儿子继承爸爸,孙子继承儿子。就是下一代继承上一代的优良传统。那么他的布局是什么呢?运行结果:
多重继承又会变成只有一张虚表,而且虚表的顺序是按照辈分排列的。也就是最开始的父类会在首位,其次才是子类。(比较符合人类特性)画图布局如下:
我们下面改一下demo重写父类的方法:
我们看到会把对应的父类的方法位置给覆盖掉。这就是6种情况的剖析。