Assembly 继承和多态

2018-12-04 17:43 更新

继承 (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处是的什么呢?这个问题的答案与如何实现多态相关。


f()函数的汇编代码


含有任意虚成员函数的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。 


示例

示例2


b1的内部表示


第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,直接调用成员函数。( 例如 ,使用早绑定)。


 C++的其它特性 

C++其它特性的工作方式( 例如 ,除了处理继承和多继承,还有运行时类型识别)不属于教程的范围。如果读者希望走得更远一些,一个好的起点是Ellis和Stroustrup写的The Annotated C++ Reference Manual和Stroustrup写的The Design and Evolution of C++。

以上内容是否对您有帮助:
在线笔记
App下载
App下载

扫描二维码

下载编程狮App

公众号
微信公众号

编程狮公众号