31.5 虚函数

还有一个简单的例子:

  1. #include <stdio.h>
  2. class object
  3. {
  4. public:
  5. int color;
  6. object() { };
  7. object (int color) { this->color=color; };
  8. virtual void dump()
  9. {
  10. printf ("color=%d", color);
  11. };
  12. };
  13. class box : public object
  14. {
  15. private:
  16. int width, height, depth;
  17. public:
  18. box(int color, int width, int height, int depth)
  19. {
  20. this->color=color;
  21. this->width=width;
  22. this->height=height;
  23. this->depth=depth;
  24. };
  25. void dump()
  26. {
  27. printf ("this is box. color=%d, width=%d, height=%d, depth=%d", color, width,height, depth);
  28. };
  29. };
  30. class sphere : public object
  31. {
  32. private:
  33. int radius;
  34. public:
  35. sphere(int color, int radius)
  36. {
  37. this->color=color;
  38. this->radius=radius;
  39. };
  40. void dump()
  41. {
  42. printf ("this is sphere. color=%d, radius=%d", color, radius);};
  43. };
  44. int main()
  45. {
  46. box b(1, 10, 20, 30);
  47. sphere s(2, 40);
  48. object *o1=&b;
  49. object *o2=&s;
  50. o1->dump();
  51. o2->dump();
  52. return 0;
  53. };

类object有一个虚函数dump(),被box和sphere类继承者替换。 如果在一个并不知道什么类型是什么对象的环境下,就像在main()这个函数里面一样,当一个虚函数dump()被调用的时候,我们还是需要知道它的返回类型的。 让我们在MSVC2008用/Ox 、 /Ob0编译看看main()的函数代码:

  1. _s$ = -32 ; size = 12
  2. _b$ = -20 ; size = 20
  3. _main PROC
  4. sub esp, 32 ; 00000020H
  5. push 30 ; 0000001eH
  6. push 20 ; 00000014H
  7. push 10 ; 0000000aH
  8. push 1
  9. lea ecx, DWORD PTR _b$[esp+48]
  10. call ??0box@@QAE@HHHH@Z ; box::box
  11. push 40 ; 00000028H
  12. push 2
  13. lea ecx, DWORD PTR _s$[esp+40]
  14. call ??0sphere@@QAE@HH@Z ; sphere::sphere
  15. mov eax, DWORD PTR _b$[esp+32]
  16. mov edx, DWORD PTR [eax]
  17. lea ecx, DWORD PTR _b$[esp+32]
  18. call edx
  19. mov eax, DWORD PTR _s$[esp+32]
  20. mov edx, DWORD PTR [eax]
  21. lea ecx, DWORD PTR _s$[esp+32]
  22. call edx
  23. xor eax, eax
  24. add esp, 32 ; 00000020H
  25. ret 0
  26. _main ENDP

指向dump()函数的指针在这个对象的某处被使用了,那么新函数的地址写到了哪里呢?只有在构造函数中有可能:其他地方都不会被main()调用。 看看类构造函数的代码:

  1. ??_R0?AVbox@@@8 DD FLAT:??_7type_info@@6B@ ; box RTTI Type Descriptor
  2. DD 00H
  3. DB ’.?AVbox@@’, 00H
  4. ??_R1A@?0A@EA@box@@8 DD FLAT:??_R0?AVbox@@@8 ; box::‘RTTI Base Class Descriptor at
  5. (0,-1,0,64)’
  6. DD 01H
  7. DD 00H
  8. DD 0ffffffffH
  9. DD 00H
  10. DD 040H
  11. DD FLAT:??_R3box@@8
  12. ??_R2box@@8 DD FLAT:??_R1A@?0A@EA@box@@8 ; box::‘RTTI Base Class Array
  13. DD FLAT:??_R1A@?0A@EA@object@@8
  14. ??_R3box@@8 DD 00H ; box::‘RTTI Class Hierarchy Descriptor
  15. DD 00H
  16. DD 02H
  17. DD FLAT:??_R2box@@8
  18. ??_R4box@@6B@ DD 00H ; box::‘RTTI Complete Object Locator
  19. DD 00H
  20. DD 00H
  21. DD FLAT:??_R0?AVbox@@@8
  22. DD FLAT:??_R3box@@8
  23. ??_7box@@6B@ DD FLAT:??_R4box@@6B@ ; box::‘vftable
  24. DD FLAT:?dump@box@@UAEXXZ
  25. _color$ = 8 ; size = 4
  26. _width$ = 12 ; size = 4
  27. _height$ = 16 ; size = 4
  28. _depth$ = 20 ; size = 4
  29. ??0box@@QAE@HHHH@Z PROC ; box::box, COMDAT
  30. ; _this$ = ecx
  31. push esi
  32. mov esi, ecx
  33. call ??0object@@QAE@XZ ; object::object
  34. mov eax, DWORD PTR _color$[esp]
  35. mov ecx, DWORD PTR _width$[esp]
  36. mov edx, DWORD PTR _height$[esp]
  37. mov DWORD PTR [esi+4], eax
  38. mov eax, DWORD PTR _depth$[esp]
  39. mov DWORD PTR [esi+16], eax
  40. mov DWORD PTR [esi], OFFSET ??_7box@@6B@
  41. mov DWORD PTR [esi+8], ecx
  42. mov DWORD PTR [esi+12], edx
  43. mov eax, esi
  44. pop esi
  45. ret 16 ; 00000010H
  46. ??0box@@QAE@HHHH@Z ENDP ; box::box

我们可以看到一些轻微的内存布局的变化:第一个域是一个指向box::vftable(这个名字由MSVC编译器生成)的指针。 在这个函数表里我们看到了一个指向box::RTTI Complete Object Locator的连接,而且还有一个指向box::dump()函数的。所以这就是被命名的虚函数表和RTTI。虚函数表可以包含所有虚函数体的地址,RTTI表包含类型的信息。另外一提,RTTI表是c++调用dynamic_cast和typeid的结果的枚举表。你可以看到这里函数名是用明文表记的。因此,一个基对象可以调用虚函数object::dump(),然后,会从这个对象的结构里调用这个继承类的函数。 枚举这些函数表需要消耗额外的CPU时间,所以可以认为虚函数比普通调用要慢一些。 在GCC生成的代码里,RTTI表的构造有些轻微的不同。