Assembly 继承和多态
继承 (Inheritance)允许一个类继承另一个类的数据和成员函数。例如, 考虑图 7.19中的代码。它展示了两个类,A和B,其中类B是通过继承类A得到的。程序的输出如下:
Size of a: 4 Offset of ad: 0
Size of b: 8 Offset of ad: 0 Offset of bd: 4 A::m()
A::m()
注意,两个类的数据成员ad(B通过继承A得到的)在相同的偏移处。这是非常重要的,因为f函数将传递一个指针到一个A对象或任意一个由A派生( 也就是 ,通过继承得到)的对象类型中。图 7.20展示了此函数的(编辑过的)汇编代码(gcc得到的)。
注意在输出中,a和b对象调用的都是A的成员函数m。从汇编程序中,我们可以看到对A::m()的调用被硬编码到函数中了。对于真正的面向对象编程,成员函数的调用取决于传递给函数的对象类型是什么。这就是所谓的 多态 。缺省情况下,C++关掉了这个特性。你可以使用virtual 关键字来激活它。图 7.21展示了如何修改这两个类。其它代码不需要修改。多态可以用许多方法来实现。不幸的是,当在以这种方法书写的时候,gcc的实现方法正处在改变中,而且与它最初的实现方法相比,明显变得更复杂了。为了简单化讨论的目的,作者只涉及基于Microsoft和Borland编译 器Windows使用的多态的实现方法。这种实现方法很多年没有改变了,而且可能在未来几年也不会改变。
有了这些改变,程序的输出如下:
Size of a: 8 Offset of ad: 4
Size of b: 12 Offset of ad: 4 Offset of bd: 8 A::m()
B::m()
现在,对f的第二次调用调用了B::m()的成员函数,因为它传递了对象B。但是,这并不是唯一的修改的地方。A的大小现在为8(而B为12)。同样,ad的偏移为4,不是0。在偏移0处是的什么呢?这个问题的答案与如何实现多态相关。
含有任意虚成员函数的C++类有一个额外的隐藏的域,它是一张指向成员函数指针数组的指针表。这个表通常称为vtable。对于A和B类,指针表储存在偏移地址0处。Windows编译器总是把此指针表放到继承树顶部 的类的开始处。从拥有虚成员函数的程序版本(源自图 7.19)中的f函数产 生的汇编代码(图 7.22)中,你可以看到对成员函数m的调用不是使用一个标号。第9行来查找对象的vtable的地址。对象的地址在第11行中被压入堆 栈。第12行通过分支到vtable里的第一个地址处来调用虚成员函数。这 次调用并不使用一个标号,它分支到EDX指向的代码地址处。这种类型的调用是一个晚绑定 (late binding)的例子。晚绑定将调用哪个成员函数的判定 延迟到代码运行时。这就允许代码为对象调用恰当的成员函数。标准的案 例(图 7.20)硬编码某个成员函数的调用,也称为 早绑定 (early binding) (因 为这儿成员函数被早绑定了,在编译的时候。)。
用心的读者将会觉得奇怪为什么在图 7.21中的类的成员函数通过使 用_ _cdecl关键字来明确声明使用的是C调用约定。缺省情况下,Microsoft对 于C++类成员函数使用的是不同的调用约定,而不是标准C调用约定。此调用约定将指向成员函数能起作用的对象的指针传递到ECX寄存器,而不 是使用堆栈。成员函数的其它明确的参数仍然使用堆栈。修改为_ _cdecl告诉编译器使用标准C调用约定。Borland C++缺省情况下使用的是C调用约定。
下面我们再看一个稍微复杂一点的例子。(图 7.23)。在这个例子中, 类A和B都有两个成员函数:m1和m2。记住因为类B并没有定义自己的成员函数m2,它继承了A类的成员函数。图 7.24展示了对象b在内存中如何储存。图 7.25展示了此程序的输出。首先,看看每个对象的vtable的地址。两个B对象的vtable地址是一样的,因此他们共享同样的vtable。一 张vtable表是类的属性而不是一个对象(就如一个static数据成员)。其次, 看看在vtable里的地址。从汇编程序的输出中,你可以确定成员函数m1指针在偏移地址 0处(或双字 0)而m2在偏移地址 4处(双字 1)。m2成员函数指针在 类A和B的vtable中是一样的,因为类B从类A继承了成员函数m2。
第25行到32行展示了你可以通过从对象的vtable读地址的方法来调用一个虚函数。成员函数地址通过一个清楚的this指针储存到了一个C类型函数指针中了。从图 7.25的输出中,你可以看到它确实可以运行。但是,请 不要像这样写代码!这只是用来举例说明虚成员函数如何使用vtable。
从这里我们可以学到一些实践的教训。一个重要的事实是当你读或写类 变量到一个二进制源文件中时,你必须非常小心。你不可以在整个对象中仅仅使用一个二进制读或写,因为可能会读或写源文件之外的vtable指针! 这是一个指向留在程序内存中的vtable的指针,而且不同的程序将不同。同样的问题会发生在C语言的结构中,但是在C语言中,结构体只有当程序员明确将指针放到结构体中时,结构体内部才有指针。类A或类B中,并没有明显地定义过指针。
再次,认识到不同的编译器实现虚成员函数的方法是不一样的是非 常重要的。在In Windows中,COM(组件对象模型,Component Object Model) 类对象使用vtable来实现COM接口。只有像Microsoft一样用来 实现虚成员函数的编译器才可以创建COM类。这也是为什么Borland采用和Microsoft一样的实现方法的原因,也是为什么不可以用gcc来创建COM类的原因之一。
虚成员函数的代码和非常虚的成员函数的代码非常相像。只是调用它们 的代码是不同的。如果汇编器能绝对保证调用哪个虚成员函数,那么它可以忽略vtable,直接调用成员函数。( 例如 ,使用早绑定)。
更多建议: