C++面向对象基础——虚函数分析

虚函数调用

调用方式

指针创建式调用函数(ClassName obj=new ClassName())调用的是虚表当中的函数地址,每个对象是不一样的,而如果是直接创建对象(ClassName obj)调用的函数地址就是同一个,都是类里面的函数。

写一个代码

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
#include"main.h"
class A
{
public:
A() {};
~A() {};
virtual void testA() {
cout << "A::TestA" << endl;
}
virtual void testB() {
cout << "A::TestB" << endl;
}
virtual void testC() {
cout << "A::TestC" << endl;
}
};
class B:public A
{
public:
B() {};
~B() {};
virtual void testA() {
cout << "B::TestA" << endl;
}
};


int main() {
A *obj1 = new A();
A *obj2 = new B();
A obj3;
obj1->testA();
obj2->testA();
obj3.testA();
}

我们观察到 obj3 是我们直接定义的,转到反汇编之后可以发现:

它并没有向上面的那么复杂地去取虚表指针然后找到地址之类的调用,而是直接传了个参数之后往类里面的这个方法去调用。

但是它们可以相互转换,例如我用 new 去创建的一个指针,我如果使用 (*obj).test() 那么则跟直接创建对象的效果是一样的,如果我是直接创建对象的话,使用 (&obj)->test() 那么跟用指针直接去调用也是一样的,在这里前者不需要调用虚表,后者需要调用虚表。

虚表指针会在构造函数之前被赋值,因此我们如果直接在构造函数中使用 memset 清空 this 的头四个字节会直接清空掉虚表指针。

我们可以看到,B 对象被构造出来虚表指针被清空为 NULL。

虚表结构

虚表结构前面讲过了,一个类若有虚函数,则它本身 +0 偏移的位置上为虚表指针(vptr)。通过这个指针过去我们可以看到一张表上面都是地址,每个地址指向了一个函数,追寻地址可以发现都是 jmp 指令,jmp之后才会跳到真正的函数代码。

所以我们可以先依照上面的理论,尝试自己做一下虚表寻址然后去调用看看。

首先我们对这个对象,先取出前四个字节的指针,也就是 vptr,其实就是拿到地址,强转 int * 类型呗。

1
(int *)&obj

然后呢,取出里面的值(),这个时候这个值还不是代码,还是一个地址,那个地址里面才是代码,所以我们再对值强转为 int 再取值,最后用函数指针接收然后调用,所以我们最后的代码长这样:

1
2
typedef void (*func)();
func f=(func)*(int*)*(int*)&obj3;

所以说还是要会指针的啊,会指针你才能很清晰地看明白上面这个式子想干嘛,或者说指定一个要求,你能很灵活地操作指针取得想要的值。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
#include"main.h"
class A
{
public:
A() {};
~A() {};
virtual void testA() {
cout << "A::TestA" << endl;
}
virtual void testB() {
cout << "A::TestB" << endl;
}
virtual void testC() {
cout << "A::TestC" << endl;
}
};
class B:public A
{
public:
B() { memset(this, 0, sizeof(B)); };
~B() {};
virtual void testA() {
cout << "B::TestA" << endl;
}
};


int main() {
A *obj1 = new A();
A *obj2 = new B();
A obj3;
typedef void (*func)();
func f=(func)*(int *)*(int*)&obj3;
f();
B obj4;
obj3 = obj4;
f = (func) * (int*)*(int*)&obj3;
f();
}
//A::TestA
//A::TestA

结果当然,两次调用都是 A 类的虚表指针,当然我们调试也可以发现,obj3在赋值的时候 vptr 没有发生变化。

虚表指针只会在我们构造的时候赋值,拷贝的时候并不会赋值,因此我如果生成了两个对象,让其中一个父类赋值另一个子类,那么我们再调用那个父类的对象则不会使用子类的虚表,这是因为父类用子类对象赋值,依然调用父类的虚表函数,是因为编译器会把子类中父类的部分,切出来给父类,但是,虚函数指针,不会覆盖,而是构造。

虚表继承

和前面是一致的,单继承情况下,子类和父类共享同一空间,所以虚表指针一样,如果多继承的话,那么虚表指针会对应偏移,与之前的类继承差不多。如果定义为 C:public A,public B的话,B具体偏移为sizeof(A)。