大数跨境
0
0

C++虚函数-虚函数指针及虚函数表深入剖析

C++虚函数-虚函数指针及虚函数表深入剖析 JOB情报局
2020-10-08
0
导读:多态分为两类:静态多态和动态多态。

前言:从系统实现的角度看,多态分为两类:静态多态和动态多态。关于多态的概念本文不做解释,请自行查阅相关资料。其中,静态多态是通过函数重载实现。要求在程序编译时就知道调用函数的全部信息,即在程序编译时系统就能决定要调用哪个函数。因此,又称静态多态为编译时多态。动态多态通过虚函数实现,不在编译时确定调用的是哪个函数,而是在程序运行过程中才动态地确定调用哪个函数,又称运行时多态。简单地说就是,将基类的指针或引用绑定到子类的实例,然后通过基类的指针或引用调用实际子类的成员函数(虚函数)。本文将介绍单继承、多重继承下虚函数的实现机制。

一、虚函数表(类表)

为了支持虚函数的机制,编译器为每一个拥有虚函数的类创建了一个虚函数表(virtual table,v-table),虚函数表是属于类的(每个包含了虚函数的类都包含一个虚函数表),而不是属于某个具体的对象,一个类只需要一个虚函数表即可,同一个类的所有对象都使用同一个虚函数表,虚函数表内的条目,即虚函数指针的赋值发生在编译器的编译阶段,也就是说在代码的编译阶段,虚函数表就可以构造出来了。这个表中有许多的槽(slot),每个槽中存放的是虚函数的地址。虚函数表解决了继承、覆盖、添加虚函数的问题,保证其真实反应实际的函数。

我们来看以下的代码。类A包含虚函数vfunc1,vfunc2,由于类A包含虚函数,故类A拥有一个虚表。

class A {
public:
   virtual void vfunc1();
   virtual void vfunc2();
   void func1();
   void func2();
private:
   int data1, data2;
};

虚函数表是一个指针数组,其元素是虚函数的指针,每个元素对应一个虚函数的函数指针。需要指出的是,普通的函数即非虚函数,其调用并不需要经过虚函数表,所以虚函数表的元素并不包括普通函数的函数指针。

为了能够找到虚函数表,对象内部包含一个指向虚函数表的指针,简称虚表指针。为了让每个包含虚函数表的类的对象都拥有一个虚函数表指针,编译器在类中添加了一个指针_vptr,用来指向虚表。这样,当类的对象在创建时便拥有了这个指针,且这个指针的值会自动被设置为指向类的虚函数表。如图所示:

上面指出,一个继承类的基类如果包含虚函数,那个这个继承类也有拥有自己的虚表,故这个继承类的对象也包含一个虚表指针,用来指向它的虚表。

下面是一个例子:

class Base
{
public:
virtual void x() { cout << "Base::x()" << endl; }
virtual void y() { cout << "Base::y()" << endl; }
virtual void z() { cout << "Base::z()" << endl; }
};

typedef void(*pFun)(void);

int main()
{
Base b;
int* vptr = (int*)&b;                     // 虚函数表地址

pFun func1 = (pFun)*((int*)*vptr);        // 第一个函数
pFun func2 = (pFun)*((int*)*vptr+1);      // 第二个函数
pFun func3 = (pFun)*((int*)*vptr+2);      // 第三个函数

func1();     // 输出Base::x()
func2();     // 输出Base::y()
func3();     // 输出Base::z()
return 0;
}

上面定义了一个Base类,其中有三个虚函数。我们将Base类对象取址 &b 并强制转换为 int*,取得虚函数表的地址。然后对虚函数表的地址取值 vptr 并强转为 int,即取得第一个虚函数的地址了。将第一个虚函数的地址加1,取得第二个虚函数的地址,再加1即取得第三个虚函数的地址。

注意:之所以可以通过对象实例的地址得到虚函数表,是因为 vptr 指针位于对象实例的最前面(这是由编译器决定的,主要是为了保证取到虚函数表有最高的性能——如果有多层继承或是多重继承的情况下)。如图所示:

二、单继承时的虚函数表

1、无虚函数覆盖

假如现有单继承关系如下:

class Base
{
public:
virtual void x() { cout << "Base::x()" << endl; }
virtual void y() { cout << "Base::y()" << endl; }
virtual void z() { cout << "Base::z()" << endl; }
};

class Derive : public Base
{
public:
virtual void x1() { cout << "Derive::x1()" << endl; }
virtual void y1() { cout << "Derive::y1()" << endl; }
virtual void z1() { cout << "Derive::z1()" << endl; }
};

int main()
{
   Base *ptr = new Derive();
   ptr->x();               //调用基类函数x()
   ptr->y();               //调用基类函数y()
   ptr->z();               //调用基类函数z()
   //ptr->y1();             //编译错误,基类Base没有成员y1
   return 0;
}

运行结果输出:

Base::x()
Base::y()
Base::z()

在这个单继承的关系中,子类没有重写父类的任何方法,而是加入了三个新的虚函数。Derive类实例的虚函数表布局如图示:


  • Derive class 继承了 Base class 中的三个虚函数,准确的说,是该函数实体的地址被拷贝到 Derive 实例的虚函数表对应的 slot 之中。

  • 新增的 虚函数 置于虚函数表的后面,并按声明顺序存放。

注:指针ptr指向派生类对象,所以ptr获取派生类的虚表指针,指向派生类的虚函数表(继承了基类的虚函数表),又因为指针ptr声明时指向的类型是Base,因此*ptr解析到的类型为Base,即仅能获取到继承基类Base的虚函数,无法获取到派生类新增的虚函数。

2、有虚函数覆盖

如果在继承关系中,子类重写了父类的虚函数:

class Base
{
public:
virtual void x() { cout << "Base::x()" << endl; }
virtual void y() { cout << "Base::y()" << endl; }
virtual void z() { cout << "Base::z()" << endl; }
};

class Derive : public Base
{
public:
virtual void x() { cout << "Derive::x()" << endl; }  // 重写
virtual void y1() { cout << "Derive::y1()" << endl; }
virtual void z1() { cout << "Derive::z1()" << endl; }
};

int main()
{
   Base *ptr = new Derive();
   ptr->x();               //调用派生类函数x()
   ptr->y();               //调用基类函数y()
   ptr->z();               //调用基类函数z()
   //ptr->y1();             //编译错误,基类Base没有成员y1
   return 0;
}

运行结果输出:

Derive::x()
Base::y()
Base::z()

则Derive类实例的虚函数表布局为:


相比于无覆盖的情况,只是把 Derive::x() 覆盖了Base::x(),即第一个槽的函数地址发生了变化,其他的没有变化。

这时,如果通过绑定了子类对象的基类指针调用函数 x(),会执行 Derive 版本的 x(),这就是多态。

三、多重继承时的虚函数表

1、无虚函数覆盖

现有如下的多重继承关系,子类没有覆盖父类的虚函数:

class Base1
{
public:
virtual void x() { cout << "Base1::x()" << endl; }
virtual void y() { cout << "Base1::y()" << endl; }
virtual void z() { cout << "Base1::z()" << endl; }
};

class Base2
{
public:
virtual void x() { cout << "Base2::x()" << endl; }
virtual void y() { cout << "Base2::y()" << endl; }
virtual void z() { cout << "Base2::z()" << endl; }
};

class Derive : public Base1, public Base2
{
public:
virtual void x1() { cout << "Derive::x1()" << endl; }
virtual void y1() { cout << "Derive::y1()" << endl; }
};
int main()
{
   Base1 *ptr1 = new Derive();
   Base2 *ptr2 = new Derive();
   ptr1->x();               //调用基类Base1函数x()
   ptr1->y();               //调用基类Base1函数y()
   ptr1->z();               //调用基类Base1函数z()
   //ptr1->y1();             //编译错误,基类Base1没有成员y1
   
   ptr2->x();               //调用基类Base2函数x()
   ptr2->y();               //调用基类Base2函数y()
   ptr2->z();               //调用基类Base2函数z()
   return 0;
}

运行结果输出:

Base1::x()
Base1::y()
Base1::z()
Base2::x()
Base2::y()
Base2::z()

对于 Derive 实例 d 的虚函数表布局,如下图:

可以看出:

  • 每个基类子对象对应一个虚函数表。

  • 派生类中新增的虚函数放到第一个虚函数表的后面。

测试代码(VS2012):

typedef void(*pFun)(void);

int main()
{
Derive b;
int** vptr = (int**)&b;                     // 虚函数表地址

// virtual table 1
pFun table1_func1 = (pFun)*((int*)*vptr+0);         // vptr[0][0]
pFun table1_func2 = (pFun)*((int*)*vptr+1);         // vptr[0][1]
pFun table1_func3 = (pFun)*((int*)*vptr+2);         // vptr[0][2]
pFun table1_func4 = (pFun)*((int*)*vptr+3);         // vptr[0][3]
pFun table1_func5 = (pFun)*((int*)*vptr+4);         // vptr[0][4]

// virtual table 2
pFun table2_func1 = (pFun)*((int*)*(vptr+1)+0);     // vptr[1][0]
pFun table2_func2 = (pFun)*((int*)*(vptr+1)+1);     // vptr[1][1]
pFun table2_func3 = (pFun)*((int*)*(vptr+1)+2);     // vptr[1][2]

// call
table1_func1();
table1_func2();
table1_func3();
table1_func4();
table1_func5();

table2_func1();
table2_func2();
table2_func3();
return 0;
}

不同编译器对 virtual table 的实现不同,经测试,在 g++ 中需要这样:

// virtual table 1
pFun table1_func1 = (pFun)*((int*)*vptr+0);         // vptr[0][0]
pFun table1_func2 = (pFun)*((int*)*vptr+2);         // vptr[0][2]
pFun table1_func3 = (pFun)*((int*)*vptr+4);         // vptr[0][4]
pFun table1_func4 = (pFun)*((int*)*vptr+6);         // vptr[0][6]
pFun table1_func5 = (pFun)*((int*)*vptr+8);         // vptr[0][8]

// virtual table 2
pFun table2_func1 = (pFun)*((int*)*(vptr+1)+0);     // vptr[1][0]
pFun table2_func2 = (pFun)*((int*)*(vptr+1)+2);     // vptr[1][2]
pFun table2_func3 = (pFun)*((int*)*(vptr+1)+4);     // vptr[1][4]

2、有虚函数覆盖

将上面的多重继承关系稍作修改,让子类重写基类的 x() 函数:

class Base1
{
public:
virtual void x() { cout << "Base1::x()" << endl; }
virtual void y() { cout << "Base1::y()" << endl; }
virtual void z() { cout << "Base1::z()" << endl; }
};

class Base2
{
public:
virtual void x() { cout << "Base2::x()" << endl; }
virtual void y() { cout << "Base2::y()" << endl; }
virtual void z() { cout << "Base2::z()" << endl; }
};

class Derive : public Base1, public Base2
{
public:
virtual void x() { cout << "Derive::x()" << endl; }     // 重写
virtual void y1() { cout << "Derive::y1()" << endl; }
};

int main()
{
   Base1 *ptr1 = new Derive();
   Base2 *ptr2 = new Derive();
   ptr1->x();               //调用基类Base1函数x()
   ptr1->y();               //调用基类Base1函数y()
   ptr1->z();               //调用基类Base1函数z()
   //ptr1->y1();             //编译错误,基类Base1没有成员y1
   
   ptr2->x();               //调用基类Base2函数x()
   ptr2->y();               //调用基类Base2函数y()
   ptr2->z();               //调用基类Base2函数z()
   return 0;
}

运行结果输出:

Derive::x()
Base1::y()
Base1::z()
Derive::x()
Base2::y()
Base2::z()

这时 Derive 实例的虚函数表布局会变成下面这个样子:

相比于无覆盖的情况,只是将Derive::x()覆盖了Base1::x()Base2::x()而已,你可以自己写测试代码测试一下,这里就不再赘述了。

注:若虚函数是 private 或 protected 的,我们照样可以通过访问虚函数表来访问这些虚函数,即上面的测试代码一样能运行。

附:编译器对指针的调整

在多重继承下,我们可以将子类实例绑定到任一父类的指针(或引用)上。以上述有覆盖的多重继承关系为例:

Derive b;
Base1* ptr1 = &b;   // 指向 b 的初始地址
Base2* ptr2 = &b;   // 指向 b 的第二个子对象
  • 因为 Base1 是第一个基类,所以 ptr1 指向的是 Derive 对象的起始地址,不需要调整指针(偏移)。

  • 因为 Base2 是第二个基类,所以必须对指针进行调整,即加上一个 offset,让 ptr2 指向 Base2 子对象。

  • 当然,上述过程是由编译器完成的。

当然,你可以在VS2012里通过Debug看出 ptr1 和 ptr2 是不同的,我们可以这样子:

Base1* b1 = (Base1*)ptr2;
b1->y();                   // 输出 Base2::y()
Base2* b2 = (Base2*)ptr1;
b2->y();                   // 输出 Base1::y()

其实,通过某个类型的指针访问某个成员时,编译器只是根据类型的定义查找这个成员所在偏移量,用这个偏移量获取成员。由于 ptr2 本来就指向 Base2 子对象的起始地址,所以b1->y()调用到的是Base2::y(),而 ptr1 本来就指向 Base1 子对象的起始地址(即 Derive对象的起始地址),所以b2->y()调用到的是Base1::y()


【声明】内容源于网络
0
0
JOB情报局
IT招聘及行业相关信息分享平台
内容 54
粉丝 0
JOB情报局 IT招聘及行业相关信息分享平台
总阅读18
粉丝0
内容54