文章出处:http://blog.csdn.net/linyt/article/details/6336762

虚函数是C++语言实现运行时多态的唯一手段,因此掌握C++虚函数也成为C++程序员是否合格的试金石。csdn网友所发的一篇博文《VC虚函数布局引发的问题》 从汇编角度分析了对象虚函数表的构,以及C++指针或者引用是如何利用这个表来实现运行时多态。

诚然,C++虚函数的结构会因编译器不同而异,但所使用的原理是一样的。为此,本文使用linux平台下的g++编译器,试图从汇编的层面上分析虚函数表的结构,以及如何利用它来实现运行时多态。

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

1. 代码

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

class Base
{
public:
virtual void f() { }
virtual void g() { }
};
class Derive : public Base
{
public:
virtual void f() {}
};
int main()
{
Derive d;
Base *pb;
pb = &d;
pb->f();
return ;

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

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

_ZTV4Base:
.long
.long _ZTI4Base
.long _ZN4Base1fEv
.long _ZN4Base1gEv
.weak _ZTS6Derive
.section .rodata._ZTS6Derive,"aG",@progbits,_ZTS6Derive,comdat
.type _ZTS6Derive, @object
.size _ZTS6Derive,

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

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

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

unsigned long Base_vtable[] = {
&Base::f(),
&Base::g(),
};

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

_ZTV6Derive:
.long
.long _ZTI6Derive
.long _ZN6Derive1fEv
.long _ZN4Base1gEv
.weak _ZTV4Base
.section .rodata._ZTV4Base,"aG",@progbits,_ZTV4Base,comdat
.align
.type _ZTV4Base, @object
.size _ZTV4Base,

相应的C语言定义如下:

unsigned long Derive_vtable[] = {
&Derive::f(),
&Base::g(),
};

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

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

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

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

_ZN4BaseC1Ev:
.LFB6:
.cfi_startproc
.cfi_personality 0x0,__gxx_personality_v0
pushl %ebp
.cfi_def_cfa_offset
movl %esp, %ebp
.cfi_offset , -
.cfi_def_cfa_register
movl (%ebp), %eax
movl $_ZTV4Base+, (%eax)
popl %ebp
ret
.cfi_endproc

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

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

struct Base {
unsigned long **vtable;
}

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

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

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

void Base::Base(struct Base *this)
{
this->vtable = &Base_vtable;
}

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

struct Derive {
unsigned long **vtable;
};
void Derive::Derive(struct Derive *this)
{
this->vtable = &Derive_vtable;
}

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

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

.globl main
.type main, @function
main:
.LFB3:
.cfi_startproc
.cfi_personality 0x0,__gxx_personality_v0
pushl %ebp
.cfi_def_cfa_offset
movl %esp, %ebp
.cfi_offset , -
.cfi_def_cfa_register
andl $-, %esp
subl $, %esp
leal (%esp), %eax
movl %eax, (%esp)
call _ZN6DeriveC1Ev
leal (%esp), %eax
movl %eax, (%esp)
movl (%esp), %eax
movl (%eax), %eax
movl (%eax), %edx
movl (%esp), %eax
movl %eax, (%esp)
call *%edx
movl $, %eax
leave
ret
.cfi_endproc

其中

andl    $-, %esp
subl $, %esp

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

Derive d;
Base *pb;
leal    (%esp), %eax
movl %eax, (%esp)
call _ZN6DeriveC1Ev

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

Derive::Dervice(&d);
leal    (%esp), %eax
movl %eax, (%esp)

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

pb = &d;

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

movl    (%esp), %eax
movl (%eax), %eax
movl (%eax), %edx
movl (%esp), %eax
movl %eax, (%esp)
call *%edx

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

pb->vtable[](bp);

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

5. 小结

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

  • 每个类都有各有的vtable结构,编译会正确填写它们的虚函数表
  • 对象在构造函数时,设置vtable值为该类的虚函数表
  • 在指针或者引用时调用虚函数,是通过object->vtable加上虚函数的offset来实现的。

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

从汇编层面深度剖析C++虚函数的更多相关文章

  1. 从源码层面深度剖析Redisson实现分布式锁的原理(全程干货,注意收藏)

    Redis实现分布式锁的原理 前面讲了Redis在实际业务场景中的应用,那么下面再来了解一下Redisson功能性场景的应用,也就是大家经常使用的分布式锁的实现场景. 引入redisson依赖 < ...

  2. C++中的继承与虚函数各种概念

    虚继承与一般继承 虚继承和一般的继承不同,一般的继承,在目前大多数的C++编译器实现的对象模型中,派生类对象会直接包含基类对象的字段.而虚继承的情况,派生类对象不会直接包含基类对象的字段,而是通过一个 ...

  3. [置顶] 【C/C++学习】之十三、虚函数剖析

    所谓虚函数,虚就虚在“推迟联编”或者“动态联编”上,一个类函数的调用并不是在编译时刻被确定的,而是在运行时刻被确定的.由于编写代码的时候并不能确定被调用的是基类的函数还是哪个派生类的函数,所以被称为“ ...

  4. 《深度探索C++对象模型》调用虚函数

    如果一个类有虚函数,那么这个类的虚函数会被放在一个虚函数表里面, 使用这个类声明的对象中,会有一个指向虚函数表的指针,当使用指向 这个对象的指针或者这个对象的引用调用一个虚函数的时候,就会从虚函数表中 ...

  5. 反汇编->C++虚函数深度分析

    先来查看一简单例子 #include<iostream> using namespace std; class Base{ public: virtual void f() { cout ...

  6. 【深度探索c++对象模型】Function语义学之虚函数

    虚函数的一般实现模型:每一个class有一个virtual table,内含该class中的virtual function的地址,然后每个object有一个vptr,指向virtual table. ...

  7. C/C++ Volatile关键词深度剖析(转)

    本文转载自博文C/C++ Volatile关键词深度剖析. 背景 前几天,发了一条如下的微博 (关于C/C++ Volatile关键词的使用建议): 此微博,引发了朋友们的大量讨论:赞同者有之:批评者 ...

  8. C++虚函数浅探

    C++中和虚函数(Virtual Function)密切相关的概念是"动态绑定"(Dynamic Binding),与之相对的概念是"静态绑定"(Static ...

  9. Objective-C类成员变量深度剖析

    目录 Non Fragile ivars 为什么Non Fragile ivars很关键 如何寻址类成员变量 真正的“如何寻址类成员变量” Non Fragile ivars布局调整 为什么Objec ...

随机推荐

  1. Linux中变量$#,$@,$0,$1,$2,$*,$$,$?的含义(转载)

    From:http://dadekey.blog.51cto.com/107327/119938 我们先写一个简单的脚本,执行以后再解释各个变量的意义   # touch variable # vi ...

  2. Citrix 服务器虚拟化之八 Xenserver虚拟机模版

    Citrix 服务器虚拟化之八 Xenserver虚拟机模版 XenServer与VMware不同,Vmware只能将现有的VM转换成模版,而XenServer具有两种方法:一种是将现有 VM 转换为 ...

  3. JAVA错误:Cannot refer to a non-final variable * inside an inner class defined in a different method

    在使用Java局部内部类或者内部类时,若该类调用了所在方法的局部变量,则该局部变量必须使用final关键字来修饰,否则将会出现编译错误“Cannot refer to a non-final vari ...

  4. linux入门学习1

    推荐 实验楼网站 在线的linux环境和课程学习 这是一些摘要和笔记 UNIX/Linux历史简介 操作系统始于二十世纪 50 年代,当时的操作系统能运行批处理程序.但是不能实现交互.交互式操作系统也 ...

  5. (medium)LeetCode 236.Lowest Common Ancestor of a Binary Tree

    Given a binary tree, find the lowest common ancestor (LCA) of two given nodes in the tree. According ...

  6. chinapay

    http://s.yanghao.org/program/viewdetail.php?i=71959 http://www.codeproject.com/csharp/biginteger.asp ...

  7. Android——进度条ProgressBar

    1.activity_progressbar.xml <?xml version="1.0" encoding="utf-8"?><Linea ...

  8. Android的消息处理机制,handler,message,looper(一)

    当应用程序启动时,Android首先会开启一个主线程(也就是UI线程),主线程为管理界面中的UI控件.在程序开发时,对于比较耗时的操作,通常会为其开辟一个单独的线程来执行,以尽可能减少用户的等待时间. ...

  9. jboss7访问日志功能及使用goaccess工具分析

    网络上虽然很多文章分别讲到jboss7的访问日志如何配置,goaccess工具怎么分析nginx/tomcat等日志.但将两者放在一起即“通过goaccess分析jboss访问日志”的倒是没搜索到. ...

  10. 关于NopCommerce3.6版的@Html.Widget(“home_page_top”)的说明

    以首页幻灯片为例子,首页幻灯片是在插件Nop.Plugin.Widgets.NivoSlider里面实现的 首页视图位置 这里其实是加载插件里面的视图内容,具体实现在插件实现 这个是扩展方法,就是执行 ...