您的位置:首页 > 编程语言 > C语言/C++

验证C++多继承下的虚函数表的布局

2017-03-19 03:04 155 查看
在深度探索C++对象模型一书第四章中,探讨了虚函数表的构建方式,例如以下定义的三个类:

class Base1 {
public:
Base1() {}
virtual ~Base1() { cout << "Base1::~Base1" << endl; }
virtual void speakClearly() { cout << "Base1::speakClearly" << endl; }
virtual Base1 *clone() const { cout << "Base1::clone" << endl; return new Base1(); }
protected:
float data_Base1;
};

class Base2 {
public:
Base2() {}
virtual ~Base2() { cout << "Base2::~Base2" << endl; }
virtual void mumble() { cout << "Base2::mumble" << endl; }
virtual Base2 *clone() const { cout << "Base2::clone" << endl; return new Base2();
}
protected:
float data_Base2;
};

class Derived : public Base1, public Base2 {
public:
Derived() {}
virtual ~Derived() { cout << "Derived::~Derived" << endl; }
virtual Derived *clone() const { cout << "Derived::clone" << endl; return new Derived(); }
virtual void thinkCarefully() { cout << "Derived::thinkCarefully" << endl; }
protected:
float data_Derived;
};


那么派生类的内存布局如图所示:



其中编译器实现的难点在于图中加星号的几个函数,三个要解决的问题是(1)virtual destructor,(2)被继承下来的Base::mumble(),(3)一组clone函数实体。这几个问题的产生都是缘于多继承中除第一个父类之外的其他父类的指针,在指向一个派生类对象时,都需要位移一段offset,例如以下的代码:

Base1 pb1 = new Derived();
Base2 pb2 = new Derived();
Derived pd = new Derived();


参考上面图片的第三个模型,其中pb1和pd均是指向对象的起始地址,而pb2则指向第二个子对象,如果不这么做,那么连最基本的非多态调用,例如访问数据 pbase->data_Base2  这样的操作都无法支持,因为pb2指向哪个对象到运行期才能知道,在编译期只能按照Base2的内存布局通过位移来访问数据成员。那么问题就清晰了:(1)通过 delete pb2 调用析构函数时,要析构的是整个派生类的对象,指针必须调整到pd的位置;(2)第二个问题则相反,当执行派生类对象指针调用从第二个父类继承下来的虚函数,例如执行
pd->mumble() 时pd需要调整位置到pb2处;(3)第三个问题与C++支持的一个语言特性有关,子类函数overwrite父类虚函数时,允许返回值类型有所不同,可以是一个publicly derived type,上述代码如果执行 pb2->clone() 创建的是Derived类型的对象,因此也会涉及到指针调整的问题。

按照书中所述,编译器实现的方式可能有更改虚表结构,引入thunk技术、“address points”策略等等,由于比较复杂并没有展开描述。总之,虚表的某些slot所指向的函数可能会发生变化,如同上图中带*的几个函数。写了一段代码来验证一下,也就是通过地址转换来访问虚表slot所指向的各个函数(一个地址可以用unsigned int型整数来表示)。为了加深理解,我在Derived里面添加了一个新函数 virtual void thinkCarefully(),事实证明它会跟在 Derived::clone()
之后,也就是说应该在 Base2::mumble() 之前。编译器为VC++2015。

typedef void (*Fun)(void);
typedef unsigned int UINT;

int main(void)
{
Fun fun, fun1, fun2, fun3, fun4;

{
Base1 b1;
cout << "--------------------------------------------------" << endl;
cout << "Address of Base1 vtable:" << (UINT *)(&b1) << endl;
cout << "Address of 1st function in Base1 vtable:" << (UINT *) *(UINT *)(&b1) << endl;

fun = (Fun)(*(UINT *) *(UINT *)(&b1));
//fun();
fun1 = (Fun)(*((UINT *) *(UINT *)(&b1) + 1));
fun1();
fun2 = (Fun)(*((UINT *) *(UINT *)(&b1) + 2));
fun2();

Base2 b2;
cout << "--------------------------------------------------" << endl;
cout << "Address of Base2 vtable:" << (UINT *)(&b2) << endl;
cout << "Address of 1st function in Base2 vtable:" << (UINT *) *(UINT *)(&b2) << endl;

fun = (Fun)(*(UINT *) *(UINT *)(&b2));
//fun();
fun1 = (Fun)(*((UINT *) *(UINT *)(&b2) + 1));
fun1();
fun2 = (Fun)(*((UINT *) *(UINT *)(&b2) + 2));
fun2();

Derived d;
cout << "--------------------------------------------------" << endl;
cout << "Address of Derived vtable:" << (UINT *)(&d) << endl;
cout << "Address of 1st function in Derived vtable:" << (UINT *) *(UINT *)(&d) << endl;

fun = (Fun)(*(UINT *) *(UINT *)(&d));
//fun();
fun1 = (Fun)(*((UINT *) *(UINT *)(&d) + 1));
fun1();
fun2 = (Fun)(*((UINT *) *(UINT *)(&d) + 2));
fun2();
fun3 = (Fun)(*((UINT *) *(UINT *)(&d) + 3));
fun3();
fun4 = (Fun)(*((UINT *) *(UINT *)(&d) + 4));
//fun4();

cout << "--------------------------------------------------" << endl;
cout << "Address of 2nd Derived vtable:" << (UINT *)(&d) + 1 << endl;
cout << "Address of 1st function in 2nd Derived vtable:" << (UINT *) *((UINT *)(&d) + 2) << endl;

fun = (Fun)(*(UINT *) *((UINT *)(&d) + 2));
//fun();
fun1 = (Fun)(*((UINT *) *((UINT *)(&d) + 2) + 1));
fun1();
fun2 = (Fun)(*((UINT *) *((UINT *)(&d) + 2) + 2));
fun2();

cout << "--------------------------------------------------" << endl;
}

std::cin.get();
return 0;
}


其中fun函数被注释掉,因为他们代表的是析构函数,为了让程序运行完当然不能执行这些函数。被注释掉的fun4对应的是第一个虚表的Base2::mumble,这里需要调整到第二个虚表的Base2::mumble,因此不是一个“简单”的函数,有可能牵扯到thunk,也有可能这里根本就是一个无效的地址(编辑器采用了其他实现方式),因此也无法在这里调用函数。令我不解的是fun2函数,它对应的是clone函数,按理说不可能所有的clone都是“单纯”的函数,但实际上将这些地址转化为 void (*)(void) 之后都成功执行了,只能理解为VC++编译器为解决这种问题做了些特殊处理。下图是程序的执行情况,包括最后栈空间内的三个对象被析构所产生的输出。

内容来自用户分享和网络整理,不保证内容的准确性,如有侵权内容,可联系管理员处理 点击这里给我发消息