c++中的虚函数-创新互联

本篇内容主要讲解“c++中的虚函数”,感兴趣的朋友不妨来看看。本文介绍的方法操作简单快捷,实用性强。下面就让小编来带大家学习“c++中的虚函数”吧!

公司主营业务:网站建设、做网站、移动网站开发等业务。帮助企业客户真正实现互联网宣传,提高企业的竞争能力。创新互联公司是一支青春激扬、勤奋敬业、活力青春激扬、勤奋敬业、活力澎湃、和谐高效的团队。公司秉承以“开放、自由、严谨、自律”为核心的企业文化,感谢他们对我们的高要求,感谢他们从不同领域给我们带来的挑战,让我们激情的团队有机会用头脑与智慧不断的给客户带来惊喜。创新互联公司推出珠晖免费做网站回馈大家。

汇编语言是难读的,特别是对一些没有汇编基础的朋友,因此,本文将汇编翻译成相应的C语言,以方便读者分析问题。

1. 代码

   为了方便表述问题,本文选取只有虚函数的两个类,当然,还有它的构造函数,如下:

[cpp] view
plaincopyprint?

  1. class Base

  2. {

  3.  public:

  4.     virtual void f() { }

  5.     virtual void g() { }

  6. };

  7. class Derive : public Base

  8. {

  9.   public:

  10.     virtual void f() {}

  11. };

  12. int main()

  13. {

  14.   Derive d;

  15.   Base *pb;

  16.   pb = &d;

  17.   pb->f();

  18.   return 0;

  19. }

2. 两个类的虚函数表(vtable)

使用g++ –Wall –S test.cpp命令,可以将上述的C++代码生成它相应的汇编代码。

[cpp] view
plaincopyprint?

  1. _ZTV4Base:

  2.     .long   0

  3.     .long   _ZTI4Base

  4.     .long   _ZN4Base1fEv

  5.     .long   _ZN4Base1gEv

  6.     .weak   _ZTS6Derive

  7.     .section    .rodata._ZTS6Derive,"aG",@progbits,_ZTS6Derive,comdat

  8.     .type   _ZTS6Derive, @object

  9.     .size   _ZTS6Derive, 8

_ZTV4Base是一个数据符号,它的命名规则是根据g++的内部规则来命名的,如果你想查看它真正表示C++的符号名,可使用c++filt命令来转换,例如:

[lyt@t468 ~]$ c++filt _ZTV4Base 
vtable for Base

_ZTV4Base符号(或者变量)可看作为一个数组,它的第一项是0,第二项_ZIT4Base是关于Base的类型信息,这与typeid有关。为方便讨论,我们略去此二项数据。 因此Base类的vtable的结构,翻译成相应的C语言定义如下:

[cpp] view
plaincopyprint?

  1. unsigned long Base_vtable[] = {

  2.     &Base::f(),

  3.     &Base::g(),

  4. };

而Derive的更是类似,只有稍为有点不同:

[cpp] view
plaincopyprint?

  1. _ZTV6Derive:

  2.     .long   0

  3.     .long   _ZTI6Derive

  4.     .long   _ZN6Derive1fEv

  5.     .long   _ZN4Base1gEv

  6.     .weak   _ZTV4Base

  7.     .section    .rodata._ZTV4Base,"aG",@progbits,_ZTV4Base,comdat

  8.     .align 8

  9.     .type   _ZTV4Base, @object

  10.     .size   _ZTV4Base, 16

相应的C语言定义如下:

[cpp] view
plaincopyprint?

  1. unsigned long Derive_vtable[] = {

  2.     &Derive::f(),

  3.     &Base::g(),

  4. };

从上面两个类的vtable可以看到,Derive的vtable中的第一项重写了Base类vtable的第一项。只要子类重写了基类的虚函数,那么子类vtable相应的项就会更改父类的vtable表项。 这一过程是编译器自动处理的,并且每个的类的vtable内容都放在数据段里面。

3. 谁让对象与vtable绑到一起

上述代码只是定义了每个类的vtable的内容,但我们知道,带有虚函数的对象在它内部都有一个vtable指针,指向这个vtable,那么是何时指定的呢? 只要看看构造函数的汇编代码,就一目了然了:

Base::Base()函数的编译代码如下:

[cpp] view
plaincopyprint?

  1. _ZN4BaseC1Ev:

  2. .LFB6:

  3.     .cfi_startproc

  4.     .cfi_personality 0x0,__gxx_personality_v0

  5.     pushl   %ebp

  6.     .cfi_def_cfa_offset 8

  7.     movl    %esp, %ebp

  8.     .cfi_offset 5, -8

  9.     .cfi_def_cfa_register 5

  10.     movl    8(%ebp), %eax

  11.     movl    $_ZTV4Base+8, (%eax)

  12.     popl    %ebp

  13.     ret

  14.     .cfi_endproc

ZN4BaseC1Ev这个符号是C++函数Base::Base() 的内部符号名,可使用c++flit将它还原。C++里的class,可以定义数据成员,函数成员两种。但转化到汇编层面时,每个对象里面真正存放的是数据成员,以及虚函数表。

在上面的Base类中,由于没有数据成员,因此它只有一个vtable指针。故Base类的定义,可以写成如下相应的C代码:

[cpp] view
plaincopyprint?

  1. struct Base {

  2.     unsigned long **vtable;

  3. }

构造函数中最关键的两句是:

    movl    8(%ebp), %eax 
    movl    $_ZTV4Base+8, (%eax)


$_ZTV4Base+8 就是Base类的虚函数表的开始位置,因此,构造函数对应的C代码如下:

[cpp] view
plaincopyprint?

  1. void Base::Base(struct Base *this)

  2. {

  3.     this->vtable = &Base_vtable;

  4. }

同样地,Derive类的构造函数如下:

[cpp] view
plaincopyprint?

  1. struct Derive {

  2.     unsigned long **vtable;

  3. };

  4. void Derive::Derive(struct Derive *this)

  5. {

  6.     this->vtable = &Derive_vtable;

  7. }

4. 实现运行时多态的最关键一步

在造构函数里面设置好的vtable的值,显然,同一类型所有对象内的vtable值都是一样的,并且永远不会改变。下面是main函数生成的汇编代码,它展示了C++如何利用vtable来实现运行时多态。

[cpp] view
plaincopyprint?

  1. .globl main

  2.     .type   main, @function

  3. main:

  4. .LFB3:

  5.     .cfi_startproc

  6.     .cfi_personality 0x0,__gxx_personality_v0

  7.     pushl   %ebp

  8.     .cfi_def_cfa_offset 8

  9.     movl    %esp, %ebp

  10.     .cfi_offset 5, -8

  11.     .cfi_def_cfa_register 5

  12.     andl    $-16, %esp

  13.     subl    $32, %esp

  14.     leal    24(%esp), %eax

  15.     movl    %eax, (%esp)

  16.     call    _ZN6DeriveC1Ev

  17.     leal    24(%esp), %eax

  18.     movl    %eax, 28(%esp)

  19.     movl    28(%esp), %eax

  20.     movl    (%eax), %eax

  21.     movl    (%eax), %edx

  22.     movl    28(%esp), %eax

  23.     movl    %eax, (%esp)

  24.     call    *%edx

  25.     movl    $0, %eax

  26.     leave

  27.     ret

  28.     .cfi_endproc

    andl    $-16, %esp 
    subl    $32, %esp

    这两句是为局部变量d和bp在堆栈上分配空间,也即如下的语句:

Derive d;   
Base *pb;

leal    24(%esp), %eax 
movl    %eax, (%esp) 
call    _ZN6DeriveC1Ev

esp+24是变量d的首地址,先将它压到堆栈上,然后调用d的构造函数,相应翻译成C语言则如下:

Derive::Dervice(&d);

leal    24(%esp), %eax 
movl    %eax, 28(%esp)

这里其实是将&d的值赋给pb,也即:

pb = &d;

最关键的代码是下面这一段:

[cpp] view
plaincopyprint?

  1. movl    28(%esp), %eax

  2. movl    (%eax), %eax

  3. movl    (%eax), %edx

  4. movl    28(%esp), %eax

  5. movl    %eax, (%esp)

  6. call    *%edx

翻译成C语言也就传神的那句:

pb->vtable[0](bp);

编译器会记住f虚函数放在vtable的第0项,这是编译时信息。

5. 小结

这里省略了很多关于编译器和C++的细枝未节,是出于讨论方便用的需要。从上面的编译代码可以看到以下信息:

1.每个类都有各有的vtable结构,编译会正确填写它们的虚函数表

2. 对象在构造函数时,设置vtable值为该类的虚函数表

3.在指针或者引用时调用虚函数,是通过object->vtable加上虚函数的offset来实现的。

当然这仅仅是g++的实现方式,它和VC++的略有不同,但原理是一样的。

到此,相信大家对“c++中的虚函数”有了更深的了解,不妨来实际操作一番吧!这里是创新互联建站,更多相关内容可以进入相关频道进行查询,关注我们,继续学习!


网站栏目:c++中的虚函数-创新互联
分享地址:http://hbruida.cn/article/diiejd.html