C++虚函数表

C++虚函数表

四月 09, 2021

C++虚函数

参考C++虚函数表解析

C++中的虚函数的作用主要是实现了多态的机制。关于多态,简而言之就是用父类型别的指针指向其子类的实例,然后通过父类的指针调用实际子类的成员函数。这种技术可以让父类的指针有“多种形态”,这是一种泛型技术。所谓泛型技术,说白了就是试图使用不变的代码来实现可变的算法。

虚函数表

C++的虚函数是通过一张虚函数表实现的,在这个表中存放的是类的虚函数的地址,这张表解决了继承、覆盖的问题,保证其容真实反应实际的函数。在有虚函数的类的实例中该类的虚函数表地址被分配在了这个实例的内存中。C++的编译器将虚函数表的地址存放在实例内存的开头,将虚函数表本身存放于只读数据段。

实例:

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
#include<iostream>
using namespace std;

class Base {
public:
virtual void f() {
cout << "Base::f" << endl;
}
virtual void g() {
cout << "Base::g" << endl;
}
virtual void h() {
cout << "Base::h" << endl;
}
};
int main() {
typedef void(*Fun)(void);
Base b;
cout << "虚函数表地址:" << (int*)*(int*)(&b) << endl;
cout << "虚函数表第一个函数地址:" << (int*)*(int*)*(int*)(&b) << endl;
Fun pFun = (Fun)*((int*)*(int*)(&b));
pFun();
}
//运行结果
//虚函数表地址:0x48e338
//虚函数表第一个函数地址:0x421c20
//Base::f

通过实例看到我们可以将实例地址&b转化成int指针int *,之后因为虚函数表在实例首地址,对其取值就能得到虚函数表的地址,再对其取值就能得到虚函数表第一个函数的地址,同理可得虚函数中其他函数的地址:

1
2
3
(Fun)*((int*)*(int*)(&b)+0);  // Base::f()
(Fun)*((int*)*(int*)(&b)+1); // Base::g()
(Fun)*((int*)*(int*)(&b)+2); // Base::h()

内存图示:

上面的图是嫖的网友的,按网友所说他在虚函数表的最后多加了一个结点,这是虚函数表的结束结点,就像字符串的结束符‘\0’一样,其标志了虚函数表的结束。这个结束标志的值在不同的编译器下是不同的。在WinXP+VS2003下,这个值是NULL。而在Ubuntu 7.10 + Linux 2.6.22 + GCC 4.1.3下,这个值是如果1,表示还有下一个虚函数表,如果值是0,表示是最后一个虚函数表。

而我在gcc version 10.2.1 20210110 (Debian 10.2.1-6)环境下调试32位程序的结果是实例首地址的存放的是vtable+8的地址,而虚函数也是从vtable+8的位置开始存,只是在vtable开始的位置有一个0,如果因为多重继承有多个vtable则第二个vtable开始位置为 -4 ,第三个为 -8 ,vtable+4的位置是该类的 typeinfo,猜测可能是把虚函数表结束标识换成开始标识了吧,因为虚函数表在内存中是相邻存放的,所以开头标识或结束标识其实没差~~吧。

一般继承(无虚函数覆盖)

若继承关系中子类没有重写任何父类的虚函数:

子类的虚函数表是这样的:

可以看到一下几点

  1. 虚函数按照声明顺序存放在表中
  2. 父类的虚函数在前,子类自己的在后

一般继承(有虚函数覆盖)

继承关系中子类有重写父类的虚函数:

子类虚函数表:

  1. 子类的虚函数表会包含父类的虚函数表
  2. 子类重写的f()函数替换了父类虚函数表中的f()
  3. 子类自己的虚函数跟在父类虚函数的后面

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

  1. 将每个父类的虚函数表拷贝形成自己的虚函数表
  2. 按照继承顺序将其放在实例内存的前n个地址(n=继承的父类数,地址宽度取决于程序位数)
  3. 子类的虚函数跟在拷贝自第一个父类的虚函数表后
  4. 需要注意的是继承自Base2和Base3的虚函数表虽然内容和父类的完全一样,但依然拷贝了一份,并没有直接使用父类的虚函数表。

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

  1. 将每个父类的虚函数表拷贝,替换其中重写的虚函数地址,形成自己的虚函数表

  2. 按照继承顺序将其放在实例内存的前n个地址

  3. 子类的虚函数跟在拷贝自第一个父类的虚函数表后

  4. 需要注意的是三个*Derive::f()*存放的其实是三个不同的地址,但只有继承自第一个父类的表中的重写函数才是函数真正的地址,其他的是一个跳转,跳转到第一个表中真正的地址,如图:

安全性

可以通过父类指针访问子类自己的虚函数

父类指针指向子类,正常来说是我无法访问子类自己的虚函数的,但可以通过虚函数表的地址找到子类的虚函数执行

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
#include<iostream>
using namespace std;

class Base {
public:
virtual void f() {
cout << "Base::f" << endl;
}
virtual void g() {
cout << "Base::g" << endl;
}
virtual void h() {
cout << "Base::h" << endl;
}
};
class Derive : public Base{
public:
virtual void f(){
cout << "Derive::f" << endl;
}
virtual void g1() {
cout << "Derive::g1" << endl;
}
};
int main() {
typedef void(*Fun)(void);
Base *pBase=new(Derive);
Fun pFun = (Fun)*((int*)*(int*)(pBase)+3);
pFun();
}
//Derive::g1

访问non-public的虚函数

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

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
#include<iostream>
using namespace std;

class Base {
private:
virtual void f() {
cout << "Base::f" << endl;
}
virtual void g() {
cout << "Base::g" << endl;
}
virtual void h() {
cout << "Base::h" << endl;
}
};
class Derive : public Base{
public:
virtual void f(){
cout << "Derive::f" << endl;
}
virtual void g1() {
cout << "Derive::g1" << endl;
}
};
int main() {
typedef void(*Fun)(void);
Base *pBase=new(Derive);
Fun pFun = (Fun)*((int*)*(int*)(pBase)+1);
pFun();
}