11
346be1d099
其他miaomiao • 于 2014-04-04发布 • 41次阅读 • (原文: www.bbsmax.com)

C++中和虚函数(Virtual Function)密切相关的概念是“动态绑定”(Dynamic Binding),与之相对的概念是“静态绑定”(Static Binding)。所谓“静态绑定”,是指在编译时就能确定函数调用语句和实际执行的函数;而“动态绑定”则是——对于同一个函数调用,编译时并不能确定具体调用的函数,直到执行时才能决定。

静态绑定

继承而没有体现多态的例子:

# include 

using namespace std;

class Base
{
public:
    void show() { cout  "I am a Base object!
"; }
};

class Derived : public Base
{
public:
    void show() { cout  "I am a Derived object!
"; }
};

int main(int argc, char *argv[])
{
    Base *pBase = new Base();
    pBase->show();

    pBase = new Derived();
    pBase->show();

    return 0;
}

这个例子将会输出:

I am a Base object!
I am a Base object!

这个例子说明,通过基类指针(或引用)调用一般的成员函数(Member Function)时(编译器)采取的都是静态绑定。

静态绑定的实现

静态绑定的实现,即一般的成员函数的实现。例如,有这样的一个类的定义:

class Simple
{
    int data;
public:
    void setData(int d) { data = d; }
    int getData() { return data; }
};

编译器会将上面的两个成员函数处理为类似下面的C代码(暂不考虑名称修饰)[1]:

// 伪代码, 说明编译器对一个成员函数定义的展开形式
void setData(Simple* this, int d)
{
    this->data = d;
}

int getData(Simple* this)
{
    return this->data;
}

(C代码?现在的编译器根本不会这么做(间接编译)!哦,只有当年的cfront才会先把C++转成C代码再编译☺)

静态绑定的实现大体如以上代码所示,即在成员函数的参数列表最前面插入一个指针(this指针),成员函数内部所有对成员变量的访问都将由此this指针寻址;这样即可实现语言层面上不同对象调用相同成员函数,访问各自的数据拷贝。

动态绑定

只需将上面静态绑定的例子上的show函数加上virtual,此时虽然Derived::show没有声明为virtual,但它也是virtual(由继承而来的virtual属性,标准就是这样规定的)。

# include 

using namespace std;

class Base
{
public:
    virtual void show() { cout  "I am a Base object!
"; }
};

class Derived : public Base
{
public:
    void show() { cout  "I am a Derived object!
"; }
};

int main(int argc, char *argv[])
{
    Base *pBase = new Base();
    pBase->show();

    pBase = new Derived();
    pBase->show();

    return 0;
}

这样,程序将输出:

I am a Base object!
I am a Derived object!

这里的例子体现了多态性,这里pBase->show()前后两次分别执行了Base::show()和Derived::show(),而且这种选择是在执行时期决定的,而非前面例子的编译时期。

一个更激进的例子是——为了证明是在执行时期,可以让pBase所指向的对象由用户输入决定:

void testRTTI()
{
    int n = 0;
    while(cin >> n) { // 遇到EOF字符结束,Windows控制台上Ctrl+Z可输入EOF,Linux Ctrl+D
        if( n % 2 )
            pBase = new Base();
        else
            pBase = new Derived();
        pBase->show();
        delete pBase;
    }
}

例如,一组输入输出(黑体是输入):

1

I am a Base object!

2

I am a Derived object!

3

I am a Base object!

动态绑定的实现

动态绑定的实现,即virtual function的实现,《深度探索C++对象模型》(以下简称)第四章对此有详细探讨[3]。这里仅简要描述,编译器在编译时将一个类的所有Virtual Function的地址存入一个表中(Virtual Table, vtbl),并在类的数据成员中安插一个指向该表的指针(vptr)。在一个继承体系中(上例中的Base和Derived),子类继承自父类的virtual function将会被安插在与父类vtbl相同的位置。编译器在该类的构造函数(若用户没有定义,编译器将生成)中插入初始化vptr的代码(将vptr指向vtbl);析构函数中也会有类似的行为。

根据的论述,上例Base和Derived的内存布局和其virtual table如下:

这些信息使得编译器可以将pBase->show();转化为:

pBase->_vptr1;

在执行时就能够实现不同的函数调用了。

VC2008跟踪

以下将上述动态绑定的代码在VC2008下以Win32 Debug版本编译,并用反汇编(调试->窗口->反汇编)调试。

1.构造函数初始化vptr

先看看Base创建的代码:

Base *pBase = new Base();
00B6152E push 4
00B61530 call operator new (0B6120Dh) ; 申请内存
00B61535 add esp,4 ; 清除压入的4
00B61538 mov dword ptr [ebp-0E0h],eax ; 保存到栈上临时变量(暂计为ret)
00B6153E cmp dword ptr [ebp-0E0h],0 ; ret和0比较
00B61545 je main+4Ah (0B6155Ah) ; 如果ret==0 不执行构造函数
00B61547 mov ecx,dword ptr [ebp-0E0h] ; ret存入 ecxthis指针
00B6154D call Base::Base (0B61136h) ; 调用Base::Base

看到了call Base::Base,继续:

00B61136 Base::Base (0B61630h)
Base::Base:
00B61630 push ebp
00B61631 mov ebp,esp
00B61633 sub esp,0CCh ; 栈上开辟空间栈向下生长
00B61639 push ebx
00B6163A push esi
00B6163B push edi
00B6163C push ecx ; 最后一个push
00B6163D lea edi,[ebp-0CCh] ; \
00B61643 mov ecx,33h ; 初始化刚开辟的空间
00B61648 mov eax,0CCCCCCCCh ; Debug版 特有代码
00B6164D rep stos dword ptr es:[edi] ; /
00B6164F pop ecx ; 最近一次 push的是 ecx而这期间esp没有被修改而上次push之前ecx也没有被修改所以ecx还是原来的值main写入的this指针
00B61650 mov dword ptr [ebp-8],ecx
00B61653 mov eax,dword ptr [this] ; 取出this指针
00B61656 mov dword ptr [eax],offset Base::`vftable' (0B67804h) ; 初始化__vptr让它指向vftable
00B6165C mov eax,dword ptr [this] ; 取出this指针写入eax
00B6165F pop edi
00B61660 pop esi
00B61661 pop ebx
00B61662 mov esp,ebp
00B61664 pop ebp
00B61665 ret

可以看到 dword ptr [eax],offset Base::`vftable' (0B67804h) 就是用来设置__vptr的,因为代码中的Base,Derived没有定义其他数据成员,所以this指针所指的dword(4B)就是__vptr。

2.virtual table里保存什么?

通过VC2008的内存窗口(调试->窗口->内存)可以查看vftable的内容:

可以看到vftable的第一个成员是:0x00B61118,第二个是0(表示结束,没有后续的),它很可能是一个函数的地址,在反汇编窗口输入改地址能看到:

由此可以看到,VC2008的vftable和一书描述的并不相同,vftable第一个slot并没有存放type_info,而是直接存了Base::show;因此这里vftable只有一个slot。

3.调用virtual function的实际代码

再来看看从调用Base::Base()到pBase->show();的代码:

00B6154D call Base::Base (0B61136h)
00B61552 mov dword ptr [ebp-0E8h],eax ; eax 里存的是this指针这相当于保存函数返回值到临时变量
00B61558 jmp main+54h (0B61564h)
00B6155A mov dword ptr [ebp-0E8h],0 ; 这行代码被忽略
00B61564 mov eax,dword ptr [ebp-0E8h] ;
00B6156A mov dword ptr [pBase],eax ; 将this保存到pBase里; 相当于 pBase = eax
    pBase->show();
00B6156D mov eax,dword ptr [pBase] ; 再取出; 相当于 eax = pBase
00B61570 mov edx,dword ptr [eax] ; 这里很关键Base::show()在vftable的slot 0所以直接取eax所指向的dword4字节
00B61572 mov esi,esp
00B61574 mov ecx,dword ptr [pBase] ; ecx 传入 this 指针
00B61577 mov eax,dword ptr [edx] ; 取出virtual function实际地址
00B61579 call eax ; 调用

至此,一个完整的virtual function的执行已经梳理清楚了。

第二次,pBase = new Derived(); 后 pBase->show(); 的流程与此完全类似,这里不再罗列;唯一不同的是,Derived::Derived()里初始化__vptr的值会是offset Derived::`vftable'。

参考

[1] 潘爱民, 张丽 译, Stanley B.Lippman, Josee Lajoie 著. C++ Primer 3e 中文版[M]. 北京:中国电力出版社, 2002. 521-523.

[2]王挺,周会平,贾丽丽,徐锡山 著. C++ 程序设计[M]. 北京:清华大学出版社,2005.374-375.

[3]侯捷 译, Stanley B.Lippman 著. 深度探索C++对象模型[M]. 北京:电子工业出版社, 2012. 152-169.

C++虚函数浅探的更多相关文章

  1. 继承虚函数浅谈 c++ 类,继承类,有虚函数的类,虚拟继承的类的内存布局,使用vs2010打印布局结果。本文笔者在青岛逛街的时候突然想到的...最近就有想写几篇关于继承虚函数的笔记,所以回家到之后就奋笔疾书的写出来发布了 应用sizeof函数求类巨细这个问题在很多面试,口试题中很轻易考,而涉及到类的时候 ...

  2. 浅谈C++虚函数很长时间都没写过博客了,主要是还没有养成思考总结的习惯,今天来一发. 我是重度拖延症患者,本来这篇总结应该是早就应该写下来的. 一.虚函数表 C++虚函数的机制想必大家都清楚了.不清楚的同学请参看各种 ...

  3. 匹夫细说C#:从园友留言到动手实现C#虚函数机制前言 上一篇文章匹夫通过CIL代码简析了一下C#函数调用的话题.虽然点击进来的童鞋并不如匹夫预料的那么多,但也还是有一些挺有质量的来自园友的回复.这不,就有一个园友提出了这样一个代码,这段代码如果被编 ...

  4. c++虚函数,纯虚函数,抽象类,覆盖,重载,隐藏C++虚函数表解析(转) ——写的真不错,忍不住转了  http://blog.csdn.net/hairetz/article/details/4137000 浅谈C++多态性  http://bl ...

  5. C++-不要在构造和析构函数中调用虚函数在实习的单位搞CxImage库时不知为什么在Debug时没有问题,但是Release版里竟然跳出个Pure virtual function call error! 啥东西呀,竟然遇上了,就探个究竟吧 ...

  6. C++中虚函数实现原理揭秘        编译器到底做了什么实现的虚函数的晚绑定呢?我们来探个究竟.      编译器对每个包含虚函数的类创建一个表(称为V TA B L E).在V TA B L E中,编译器放置特定类的虚函 ...

  7. C++虚函数和函数指针一起使用C++虚函数和函数指针一起使用,写起来有点麻烦. 下面贴出一份示例代码,可作参考.(需要支持C++11编译) #include #include

  8. 【C++】多态性(函数重载与虚函数)多态性就是同一符号或名字在不同情况下具有不同解释的现象.多态性有两种表现形式: 编译时多态性:同一对象收到相同的消息却产生不同的函数调用,一般通过函数重载来实现,在编译时就实现了绑定,属于静态绑定. ...

  9. 虚函数的使用 以及虚函数与重载的关系, 空虚函数的作用,纯虚函数->抽象类,基类虚析构函数使释放对象更彻底为了访问公有派生类的特定成员,可以通过讲基类指针显示转换为派生类指针. 也可以将基类的非静态成员函数定义为虚函数(在函数前加上virtual) #include usi ...

随机推荐

  1. Atitit 延迟绑定架构法attilax总结Atitit 延迟绑定架构法attilax总结 配置文件的延迟绑定1 Api属性与方法的回调延迟绑定1 后期绑定和前期绑定2 延迟调用2 用 Java 语言延迟绑定2 什么是推迟绑定 C++3 配置文 ...

  2. Xamarin Android Activity全屏的两种方式方式一 直接在Activity的Attribute中定义 如下 在 MainActivity 中 [Activity(Label = "app", MainLauncher = t ...

  3. viewpaper引用:http://blog.csdn.net/billpig/article/details/6650097 增加回弹 http://www.apkbus.com/android-78437-1-1 ...

  4. 数据库的点数据根据行政区shp来进行行政区处理,python定时器实现# -*- coding: utf-8 -*- import struct import decimal import itertools import arcpy import math impor ...

  5. WPF Dispatcher 一次小重构几个月之前因为项目需要,需要实现一个类似于WPF Dispatcher类的类,来实现一些线程的调度.之前因为一直做Asp.Net,根本没有钻到这个层次去,做的过程中,诸多不顺,重构了四五次,终于实现, ...

  6. ggplot2颜色操作1.颜色列表

  7. Unity3d 经验小结  Unity3d 经验小结 文本教程 你是第2541个围观者 0条评论 供稿者:Jamesgary 标签:unity3d教程 Fbx.贴图导入Unity时的注意事项: 在导出Fbx之前,Maya中已 ...

  8. linux根分区扩容Linux 根分区扩容 1.fdisk –l  (红线部分为新添加的硬盘) 2.磁盘格式化 3. mkfs.ext3 -T largefile /dev/sde(格式化上面的分区) 4. vgdisp ...

  9. php升级5.3到5.4,5.5,5.6Laravel要求php大于5.5.9,升级php5.3.3到5.6.14(最新版为 5.6.15 ) Add EPEL and Remi repositories onto your system: ...

  10. Oracle数值处理函数 (绝对值、取整...)1.绝对值:abs()    select abs(-2) value from dual; 2.取整函数(大):ceil()    select ceil(-2.001) value from du ...

暂无回复。
需要 登录 后方可回复, 如果你还没有账号请点击这里 注册
Top