虚表指针在哪里

虚表指针

群里有人问。就写了

从最简单的类开始, 后面有单继承和多继承虚表指针的不同

代码在32环境下:

先写结论:

         虚表指针在构造函数与析构中赋值(下面反汇编证明), 赋值: *this = 虚表指针(即首个成员)

         虚表指针指向的是一个数组,存放虚函数,虚函数按一般声明先后排序.

         这个数组一般在 .rdata 或 .data

         每个父类都会在构造,析构中设置自己的虚表,下面继承中会写

        * 为什么要在构造和析构中都赋值, 在下面的继承中会写.
 

下面代码中顺带一些内存布局

虚表指针在构造函数中 *this = 虚表指针, 指向数组[ virtual void t ]

成员变量a 在 this + 4 的地方,跟随在虚表后, 若没有虚表的类则放在this上


#include <iostream>
#include <Windows.h>
#include <stdio.h>
using std::cout;
using std::endl;

class A
{
public:
	A() {		
		a = 1;
	}
	~A() {
	}
	virtual void t() {
		cout << "t" << endl;
	}
	int a;
};

int main()
{
	cout <<"size:" << sizeof(A) << endl;
	A a;
	DWORD * ptr = (DWORD*)&a;
	DWORD vptr_addr = *ptr;
	//虚表在首个成员的地址上 , 即 *this
	cout << "虚表:" << std::hex << vptr_addr<< endl;

	// 因为vptr 占了首个成员的位置, 成员a跟在虚表后面
	int member = *(ptr + 1);
	cout << "member:" << member << endl;

	//通过虚表找到函数地址,只有一个虚函数,因此偏移0
	DWORD * arr_func = (DWORD*)vptr_addr;
	void (*func)() = (void (*)())(*(arr_func + 0)); // arr_func[0]
	cout << "函数地址:" << func << endl;
	func();

	return 0;
}

看反汇编的构造 析构函数 , 删掉了不相关的, 只看虚表指针在构造和析构中赋值:

在这2个函数中赋值都是与继承相关,防止调用错误的虚函数

A(){

mov         dword ptr [this],ecx   // ecx 为 this
mov         eax,dword ptr [this]   // eax = this

// *this =  A的虚表指针 ,这里赋值
mov         dword ptr [eax],offset A::`vftable' (0419B34h) 
    
}

虚表指针: 0x00419B34
内存数据: 0x00419B34  9a 11 41 00 00 00 00 00
还原数组: [0x0041119a] -> [virtual t]


// 在析构中同样出现
~A(){

mov         dword ptr [this],ecx   // ecx == this
mov         eax,dword ptr [this]   // eax = this

// *this = A的虚表指针
mov         dword ptr [eax],offset A::`vftable' (0419B34h)
}

我这里的vptr为 0x00419B34 , 首个函数地址 0x0041119a -> 虚函数t 的地址;

单继承:

父类会在构造函数中把 *this = 父类自己的虚表指针指向自己的数组,[virtual ~A, virtual A::t]

子类在构造中会把 *this  = 子类自己的虚表指针,数组[virtual ~B, virtual A::t]

虚函数在数组中的排序一般按声明先后顺序

数据成员按继承先后顺序排放,即 虚表 , a , b

在父类中构造和析构中 设置 *this = 父类虚表 , 为了防止调用虚函数出错.

现在假设:

        1.子类如果有 virtual void t, 则子类虚表数组:[virtual ~B, virtual B::t ]

        2. 如果在父类构造和析构调用t(),且没有设置 *this = 父类虚表,

            则父类就会调用到子类的 virtual void t

        


#include <iostream>
#include <Windows.h>
#include <stdio.h>
using std::cout;
using std::endl;

class A
{
public:
	A() {		
		a = 1;
	}

	virtual ~A() { // 虚析构
	}

	virtual void t() {
		cout << "t" << endl;
	}
	int a;
};
class B : public A
{
public:
	B() {
		b = 2;
	}
	~B() {

	}
	int b;
};

int main()
{
	cout <<"size:" << sizeof(B) << endl;
	B b;
	DWORD * ptr = (DWORD*)&b;
	DWORD vptr_addr = *ptr;
	cout << "虚表:" <<std::hex <<vptr_addr << endl;

	//获取成员, 继承顺序的原因 : A的东西放在前, 自己的东西放在后
	int member_in_class_a = *(ptr + 1);
	int member_in_class_b = *(ptr + 2);
	cout << "A::a :" << member_in_class_a << ", B::b:" << member_in_class_b << endl;

	// 获取子类虚表指针, 调用函数t, 第一个是析构, 第二个函数t
	DWORD * arr_func = (DWORD*)vptr_addr;
	void(*func)() = (void(*)())(arr_func[1]);
	cout << "函数地址:" << std::hex <<func << endl;
	func();


	return 0;
}

反汇编, 构造函数, 去掉了不相关的:

A(){
mov         dword ptr [this],ecx  // ecx = this指针
mov         eax,dword ptr [this]  // eax = this

// *this = 父类虚表指针 0x00419B34
mov         dword ptr [eax],offset A::`vftable' (0419B34h) 
}

A的虚表地址: 0x00419B34  
A的虚表内存数据:0x00419B34  65 14 41 00 60 14 41 00
还原数组:[0x00411465,0x00411460] -> [virtual ~A , virtual A::t ]
对应VSdebug:
0x00411465 {virtualptr.exe!A::`vector deleting destructor'(unsigned int)} // 析构 ~A
0x00411460 {virtualptr.exe!A::t(void)} // A::t


B() {

mov         ecx,dword ptr [this]    // eax = this
call        A::A (041146Fh)         // 调用父类构造
mov         eax,dword ptr [this]    // eax = this

// *this = 子类虚表指针 0x00419B44
mov         dword ptr [eax],offset B::`vftable' (0419B44h)  

}

B的虚表地址:0x00419B44
虚表内存数据:0x00419B44  51 14 41 00 60 14 41 00 
数组:[0x00411451, 00411460] -> [virtual ~B , virtual A::t]
对应VS debug:
0x00411451 {virtualptr.exe!B::`vector deleting destructor'(unsigned int)} // 析构~B
0x00411460 {virtualptr.exe!A::t(void)}	// A::t

可以看到:

父类虚表指向自己的数组:[virtual ~A, virtual A::t]

子类指向自己的数组: [virtual ~B, virtual A:t]

防止父类调用了子类虚函数

析构函数反汇编, 也是同样的操作,防止在析构中调用子类虚函数

~B() {
mov         dword ptr [this],ecx  // ecx == this
mov         eax,dword ptr [this]  // eax  = this 

// *this = B的虚表指针
mov         dword ptr [eax],offset B::`vftable' (0419B44h)  

// ecx = this
mov         ecx,dword ptr [this]  

// 调用父类析构
call        A::~A (041144Ch)  
}


~A(){
mov         dword ptr [this],ecx  // ecx == this

// eax = this
mov         eax,dword ptr [this]  

// *this = A的虚表
mov         dword ptr [eax],offset A::`vftable' (0419B34h)  
}

做个测试?

现在假设, 还有一个基类:

class Base {
public:
	Base() {
		base = 111;
	}
	virtual ~Base() {
	}
	int base;
};

class A : public Base{
//与上面一样
}
class B: public A{
//与上面一样
}

这时会有3个虚表 , Base的构造析构 还会赋值一次Base自己的虚表;

Base 虚表-> [virtual ~Base]

A 虚表 -> [virtual ~A , virtual A::t]

B 虚表 -> [virtual ~B, virtual A::t]

内存布局 , 上面说过按继承方式排放:

首地址: 虚表, Base::base, A::a, B::b

如果把Base改成这样 , 没有虚函数了:

class Base {
public:
	Base() {
		base = 111;
	}
	~Base() {
	}
	int base;
};

这时. Base没有虚表了其构造和析构也不会赋值自己虚表

 A虚表和B虚表不变

内存布局也不变

例如:

假如
int main(){
    B b;  // &b ==  0x0018FEE4

    b的内存数据
    0x0018FEE4  48 9b 41 00 6f 00 00 00 01 00 00 00 02 00 00 00
    
    vs debug对应的数据:
+		__vfptr	0x00419b48 {virtualptr.exe!void(* B::`vftable'[3])()} 
		base	0x0000006f	int
		a	0x00000001	int
		b	0x00000002	int
}



多继承:

上一篇:带有虚函数的class的size如何计算?


下一篇:MySQL 5.7 虚拟列 (virtual columns)