【C++编程】C++虚函数表剖析 ①
概述
为了实现C++的多态,C++使用了一种动态绑定的技术。这个技术的核心是虚函数表(下文简称虚表)。本文介绍虚函数表是如何实现动态绑定的。
C++多态实现的原理:
• 当类中声明虚函数时,编译器会在类中生成一个虚函数表 • 虚函数表是一个存储成员函数地址的数据结构 • 虚函数表是由编译器自动生成与维护的 • virtual成员函数会被编译器放入虚函数表中 • 存在虚函数表时,每个对象中都有一个指向虚函数表的指针 |
注意:
- 每个包含了虚函数的类,都会有虚函数表。
- 每个类只有一个虚函数表,每个类的对象内会有一个虚函数指针指向这个虚函数表。
- 一个类继承另一个类时,会继承这个类的虚函数,所以一个类继承了包含虚函数的基类,那么这个类也会有虚函数,也会有虚函数表。所以如果一个基类包含了虚函数,那么其继承类也可调用这些虚函数,换句话说,一个类继承了包含虚函数的基类,那么这个类也拥有自己的虚表。
- 虚函数表的指针赋值发生在编译期。
一、类的虚表
我们来看以下的代码。类A包含虚函数vfunc1,vfunc2,由于类A包含虚函数,故类A拥有一个虚表。
class A {
public:
virtual void vfunc1();
virtual void vfunc2();
void func1();
void func2();
private:
int m_data1, m_data2;
};
虚表是一个指针数组,其元素是虚函数的指针,每个元素对应一个虚函数的函数指针。需要指出的是,普通的函数即非虚函数,其调用并不需要经过虚表,所以虚表的元素并不包括普通函数的函数指针。
虚表内的条目,即虚函数指针的赋值发生在编译器的编译阶段,也就是说在代码的编译阶段,虚表就可以构造出来了。
二、类的对象的虚函数表指针
虚表是属于类的,而不是属于某个具体的对象,一个类只需要一个虚表即可。同一个类的所有对象都使用同一个虚表。
为了指定对象的虚表,每个对象的内部包含一个虚表的指针,来指向自己所使用的虚表。为了让每个包含虚表的类的对象都拥有一个虚表指针,编译器在类中添加了一个指针 *__vptr
,用来指向虚表。这样,当类的对象在创建时便拥有了这个指针,且这个指针的值会自动被设置为指向类的虚表。
图2:对象与它的虚表
上面指出,一个继承类的基类如果包含虚函数,那个这个继承类也有拥有自己的虚表,故这个继承类的对象也包含一个虚表指针,用来指向它的虚表。
动态绑定:说到这里,大家一定会好奇C++是如何利用虚表和虚表指针来实现动态绑定的。我们先看下面的代码。
class A {
public:
virtual void vfunc1();
virtual void vfunc2();
void func1();
void func2();
private:
int m_data1, m_data2;
};
class B : public A {
public:
virtual void vfunc1();
void func1();
private:
int m_data3;
};
class C: public B {
public:
virtual void vfunc2();
void func2();
private:
int m_data1, m_data4;
};
类A是基类,类B继承类A,类C又继承类B。类A,类B,类C,其对象模型如下图所示。
由于这三个类都有虚函数,故编译器为每个类都创建了一个虚表,即类A的虚表(A vtbl),类B的虚表(B vtbl),类C的虚表(C vtbl)。类A,类B,类C的对象都拥有一个虚表指针,*__vptr,用来指向自己所属类的虚表。
- 类A包括两个虚函数,故A vtbl包含两个指针,分别指向 A::vfunc1() 和 A::vfunc2() 。
- 类B继承于类A,故类B可以调用类A的函数,但由于类B重写了函数 B::vfunc1() ,故B vtbl的两个指针分别指向 B::vfunc1() 和 B::vfunc1() 。
- 类C继承于类B,故类C可以调用类B的函数,但由于类C重写了 C::vfunc2() 函数,故C vtbl的两个指针分别指向 B::vfunc1() 和 C::vfunc2() 。
虽然图3看起来有点复杂,但是只要抓住“对象的虚表指针用来指向自己所属类的虚表,虚表中的指针会指向其继承的最近的一个类的虚函数”这个特点,便可以快速将这几个类的对象模型在自己的脑海中描绘出来。[非虚函数的调用不用经过虚表,故不需要虚表中的指针指向这些函数。
【注意】非虚函数的调用不用经过虚表,故不需要虚表中的指针指向这些函数。
一般继承(无虚函数覆盖)
下面,再让我们来看看继承时的虚函数表是什么样的。假设有如下所示的一个继承关系:
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 << "Base::f1()" << endl; }
virtual void g1() { cout << "Base::g1()" << endl; }
virtual void h1() { cout << "Base::h1()" << endl; }
};
图解:
请注意,在这个继承关系中,子类没有重载任何父类的函数。那么,在派生类的实例中,其虚函数表如下所示:
对于实例:Derive d; 的虚函数表如下:
我们可以看到下面几点:
- 虚函数按照其声明顺序放于表中。
- 父类的虚函数在子类的虚函数前面。
一般继承(有虚函数覆盖)
覆盖父类的虚函数是很显然的事情,不然,虚函数就变得毫无意义。下面,我们来看一下,如果子类中有虚函数重载了父类的虚函数,会是一个什么样子?假设,我们有下面这样的一个继承关系。
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 << "Base::f1()" << endl; }
virtual void g1() { cout << "Base::g1()" << endl; }
virtual void h1() { cout << "Base::h1()" << endl; }
};
图解:
为了让大家看到被继承过后的效果,在这个类的设计中,我只覆盖了父类的一个函数:f()。那么,对于派生类的实例,其虚函数表如下所示:
我们从表中可以看到下面几点,
- 覆盖的f()函数被放到了虚表中原来父类虚函数的位置。
- 没有被覆盖的函数依旧。
这样,我们就可以看到对于下面这样的程序:
Base *b = new Derive();
b->f();
由b所指的内存中的虚函数表的f()的位置已经被Derive::f()函数地址所取代,于是在实际调用发生时,是Derive::f()被调用了。这就实现了多态。
安全性
每次写C++的文章,总免不了要批判一下C++。这篇文章也不例外。通过上面的讲述,相信我们对虚函数表有一个比较细致的了解了。水可载舟,亦可覆舟。下面,让我们来看看我们可以用虚函数表来干点什么坏事吧。
一、通过父类型的指针访问子类自己的虚函数
我们知道,子类没有重载父类的虚函数是一件毫无意义的事情。因为多态也是要基于函数重载的。虽然在上面的图中我们可以看到Base1的虚表中有Derive的虚函数,但我们根本不可能使用下面的语句来调用子类的自有虚函数:
Base1 *b1 = new Derive(); b1->f1(); //编译出错
任何妄图使用父类指针想调用子类中的未覆盖父类的成员函数的行为都会被编译器视为非法,所以,这样的程序根本无法编译通过。但在运行时,我们可以通过指针的方式访问虚函数表来达到违反C++语义的行为。(关于这方面的尝试,通过阅读后面附录的代码,相信你可以做到这一点)
二、访问non-public的虚函数
另外,如果父类的虚函数是private或是protected的,但这些非public的虚函数同样会存在于虚函数表中,所以,我们同样可以使用访问虚函数表的方式来访问这些non-public的虚函数,这是很容易做到的。如:
1 #include<iostream> 2 using namespace std; 3 4 class Base { 5 private: 6 virtual void f() { cout << "Base::f" << endl; } 7 8 }; 9 10 class Derive : public Base { 11 12 }; 13 14 typedef void(*Fun)(void); 15 16 int main() 17 { 18 Derive d; 19 Fun pFun = (Fun)*((int*)*(int*)(&d) + 0); 20 pFun(); 21 }
输出结果:
单一的一般继承
在这个继承关系中,父类,子类,孙子类都有自己的一个成员变量。而了类覆盖了父类的f()方法,孙子类覆盖了子类的g_child()及其超类的f()。
源代码如下所示:
class Parent
{
public:
int iparent;
Parent ():iparent (10) {}
virtual void f() { cout << " Parent::f()" << endl; }
virtual void g() { cout << " Parent::g()" << endl; }
virtual void h() { cout << " Parent::h()" << endl; }
};
class Child : public Parent
{
public:
int ichild;
Child():ichild(100) {}
virtual void f() { cout << "Child::f()" << endl; }
virtual void g_child() { cout << "Child::g_child()" << endl; }
virtual void h_child() { cout << "Child::h_child()" << endl; }
};
class GrandChild : public Child
{
public:
int igrandchild;
GrandChild():igrandchild(1000) {}
virtual void f() { cout << "GrandChild::f()" << endl; }
virtual void g_child() { cout << "GrandChild::g_child()" << endl; }
virtual void h_grandchild() { cout << "GrandChild::h_grandchild()" << endl; }
};
图解:
对象的内存布局:
可见以下几个方面:
- 虚函数表在最前面的位置。
- 成员变量根据其继承和声明顺序依次放在后面。
- 在单一的继承中,被overwrite的虚函数在虚函数表中得到了更新。
#include <iostream>
class Base
{
public:
int i;
virtual void Print() { cout << "Base::Print"; }
};
class Derived : public Base
{
public:
int n;
virtual void Print() { cout << "Derived::Print" << endl;}
}
int main()
{
Derived d;
cout << sizeof(Base) << "," << sizeof(Derived);
reuturn 0;
}
综合
1. 例题:
#include <iostream>
class Base
{
public:
int i;
virtual void Print() { cout << "Base::Print"; }
};
class Derived : public Base
{
public:
int n;
virtual void Print() { cout << "Derived::Print" << endl;}
}
int main()
{
Derived d;
cout << sizeof(Base) << "," << sizeof(Derived);
reuturn 0;
}
参考资料
- C++虚函数表剖析
- C++入门学习——虚函数表介绍
- C++ 虚函数表解析 陈皓著
- C++ 对象的内存布局 陈皓著
- 从内存布局看C++虚继承的实现原理
- C++对象模型(五):The Semantics of Data Data语义学
- https://hongkg.cn/2021/09/05/C-%E8%99%9A%E5%87%BD%E6%95%B0%E8%A1%A8/
- https://www.cnblogs.com/lfri/p/12717971.html
- C++ 虚函数表