c++虚表

1.虚表


  基类指针可以指向基类对象,也可以指向派生类对象,实现多态的时候,是用基类指针指向一个派生类对象。

  在同一个基类指针上调用同一个虚函数,会因为基类指针实际指向的对象不同而调用不同的函数,这就是所谓多态,基类指针只不过是个普通指针变量而已,所以靠这个指针是无法知道究竟应该调用哪个函数的。从头到尾只有指向的对象不同,明确这一点很重要,这说明只有对象本身可以确定多态时究竟应该调用哪个函数,即所谓动态绑定,虚函数表即是存在于对象中,基类对象的虚函数表和派生类是不同的,不同的虚函数表最终指引到不同的函数调用。

  虚函数的地址存放于虚函数表之中,因此,虚函数的调用会被编译器转换为对虚函数表的访问。

  C++的编译器把虚函数表的指针存在于对象实例中最前面的位置(这是为了保证取到虚函数表的有最高的性能——如果有多层继承或是多重继承的情况下)。虚表指针的名字也会被编译器更改,所以在多继承的情况下,类的内部可能存在多个虚表指针。通过不同的名字被编译器标识。虚函数表中可能还存在其他的内容,如用于RTTI的type_info类型。

  每个有虚函数的类或者虚继承的子类,编译器都会为它生成一个虚表。同时,子类继承父类时,会获得继承下来的__vptr,就像继承普通成员变量一样。

2.虚表的作用和布局


  【1】单继承

    假设z()函数在基类虚函数表中的索引为4,要如何来保证在执行期基类指针ptr调用的是正确的z()实体?其中微妙在于,编译将做一个小小的转换,即编译器把:

ptr->z();

    转换为:

(*ptr->vptr[4])(ptr);  //第二个ptr表示this指针

    这个转换保证了调用到正确的函数实体(多态),因为:虽然我们不知道ptr所指的真正类型,但它可以通过vptr找到正确类型的虚函数表。在整个继承体系中z()的地址总是被放在slot 4。

    这种情况下,派生类中仅有一个虚函数表。如果派生类没有重写基类的虚函数的话,派生类自定义的新的虚函数会附加到虚表的后面。如果有重写基类的虚函数,则会覆盖基类的虚函数。

    [1]没有重写任何虚函数

                          

    [2]有重写虚函数

                            

  【2】多重继承:

    多继承情况下,派生类中有多个虚函数表,虚函数的排列方式和继承的顺序一致。派生类重写函数将会覆盖所有虚函数表的同名内容,派生类自定义的新的虚函数会附加到第一个类的虚函数表的后面,这个虚表是主虚表,后面的是次要虚表。

    多重继承下的对象内存布局:

      

    调用虚函数时查找虚表的方式,即调用主虚表的函数时使用的vptr不需要偏移,而调用次虚表的函数时使用的vptr需要有偏移,例如:

class A1{
public:
	virtual void a1_fun1() { cout << "A1::a1_fun1" << endl; }
};

class A2{
public:
	virtual void a2_fun1() { cout << "A2::a2_fun1" << endl; }
};

class B : public A1, public A2{
public:
	virtual void b_fun1(){ cout << "B::b_fun1" << endl; }
};

int main(){
	B *b = new B;	//或者A1 *b = new B;
	b->a1_fun1();
	return 0;
}

    对应的IDA反汇编为:

int __cdecl main(int argc, const char **argv, const char **envp)
{
  void (__thiscall ***v4)(_DWORD); // [esp+Ch] [ebp-DCh]
  B *v5; // [esp+14h] [ebp-D4h]
  void (__thiscall ***v6)(_DWORD); // [esp+E0h] [ebp-8h]

  v5 = (B *)operator new(8u);						//两个vptr指针,因此大小是8
  if ( v5 )
    v4 = (void (__thiscall ***)(_DWORD))B::B(v5);	//v4就是对象的起始地址
  else
    v4 = 0;
  v6 = v4;
  (**v4)(v4);										//两次**解引用起始地址v4(也就是vptr)得到第一个虚函数地址,第二个v4表示this
  return 0;
}

    而如果把b->a1_fun1();替换为b->a2_fun1();,此时表示调用第二个基类的虚函数,则此时的IDA为:

int __cdecl main(int argc, const char **argv, const char **envp)
{
  int v4; // [esp+Ch] [ebp-DCh]
  B *v5; // [esp+14h] [ebp-D4h]
  int v6; // [esp+E0h] [ebp-8h]

  v5 = (B *)operator new(8u);							//两个vptr指针,因此大小是8
  if ( v5 )
    v4 = B::B(v5);										//v4就是对象的起始地址
  else
    v4 = 0;
  v6 = v4;
  (**(void (__thiscall ***)(int))(v4 + 4))(v4 + 4);		//(v4 + 4)会得到第二个基类的vptr,第二个(v4 + 4)表示this,即类A2的对象
  return 0;
}

    [1]无虚函数覆盖

            

    [2]有虚函数覆盖

          

    注意以上的图,多个虚表指针vptr的地址是连续的,这是不对的,每个vptr后面都连续保存了各个基类的成员变量。

3.问题


  问题1:使用父类指针调用子类中的未覆盖父类的成员函数的行为都会被编译器视为非法,所以,这样的程序根本无法编译通过。但在运行时,我们可以通过指针的方式访问虚函数表来达到此目的。

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

  问题3:派生类继承两个基类时,如果第一个基类没有虚函数(虚表),则会把有虚函数的类放到前面。

  问题4:基类的析构函数是虚函数时,delete基类指针(指向子类对象)会调用到子类的虚构函数,这时候子类并没有重写基类的析构函数,这是为什么?虽然基类和子类的析构函数名字不一样,但是查看虚表发现子类的析构函数会覆盖基类的析构函数的slot,此时可以当做编译器将所有的析构函数都重新命名为了destructor()。

4.vs2013下查看虚表


   测试代码:

#include<iostream>
#include<vector>
#include <stdlib.h>
using namespace std;

class A1
{
	public:
		A1(int _a1 = 1) : a1(_a1) { }
		virtual void f() { cout << "A1::f" << endl; }
		virtual void g() { cout << "A1::g" << endl; }
		virtual void h() { cout << "A1::h" << endl; }
		~A1() {}
	private:
		int a1;
};
class A2
{
	public:
		A2(int _a2 = 2) : a2(_a2) { }
		virtual void f() { cout << "A2::f" << endl; }
		virtual void g() { cout << "A2::g" << endl; }
		virtual void h() { cout << "A2::h" << endl; }
		
		~A2() {}
	private:
		int a2;
};
class A3
{
	public:
		A3(int _a3 = 3) : a3(_a3) { }
		virtual void f() { cout << "A3::f" << endl; }
		virtual void g() { cout << "A3::g" << endl; }
		virtual void h() { cout << "A3::h" << endl; }
		~A3() {}
	private:
		int a3;
};

class B : public A1, public A2, public A3
{
public:
	B(int _a1 = 1, int _a2 = 2, int _a3 = 3, int _b = 4) :A1(_a1), A2(_a2), A3(_a3), b(_b) { }
	virtual void f1(){ cout << "B::f" << endl; }
	virtual void g1(){ cout << "B::g" << endl; }
	virtual void h1(){ cout << "B::h" << endl; }
	
private:
	int b;
};


int main()
{
	B b;

	//在此插入断点后,启动调试,打开调试->窗口->局部变量,可以看到虚表
	return 0;
}

  

   事实上vs调试并不能看到完整信息,比如第一个虚表A1后面的自定义的虚函数。

 

 

 

 

 

 

 

 

posted on 2019-03-14 09:39  能量星星  阅读(1077)  评论(0编辑  收藏  举报

导航