个人管理功能

在CSDN Blog撰写技术文章,即有机会入选CSDN技术中心,现在就去免费注册!已注册用户,点击登录

搜索
热门标签
专题历史

有人说SOA是一种IT策略,有人说SOA是一种架构理念,还有人说SOA是一种服务。SOA到底是什么?它将带来什么?软件产业的变革亦或是新的机遇下的挑战?业界权威专家带领我们一起去深究,去探索。BEA三位重量级专家与您共同探讨SOA

随着WPF/E更名为正式名称Silverlight,以及Silverlight 1.1 Alpha 版本的发布,答案变得清晰,而且令人兴奋! - 一个跨操作系统,跨浏览器的Web应用平台出现了。Silverlight 这样一个4.5MB的浏览器插件(1.1 Alpha文件)是如何做到的这些的?周岳: SilverLight-Web应用的一道强光

中国移动用户数量在大踏步地发展与增长,根据产业部的数据,仅三月就新增了670万户。预估计6月份之后,中国很快将迎来第五亿手机用户(平均不到3人拥有一台手机)而J2ME做为最重要的手机跨平台技术,凭借Java平台以其良好的开放性和支持能力,得到了众多手机厂商的支持。对众多开发者来说,J2ME程序易于移植,轻松实现“一次编写,到处运行”。J2ME系列开发专题,将带你从最基本的工具安装,环境配置开始,进入移动应用开发的世界。
 
CSDN移动开发系列之-“J2ME开发实训”

7月31日-8月1日,即将在上海召开甲骨文全球大会•亚太地区会议同期举行的甲骨文开发者大会,这是一项付费参加的面向开发人员的活动。在甲骨文开发者大会期间,您将听到世界一流的专家讲述如何使用Java、.NET、XML和PL/SQL以及Ajax、PHP、Spring、Groovy on Rails等流行技术来简化开发过程。在为期两天的甲骨文开发者大会中,您将能够提高自身的开发技能,扩充知识,参加几十场由专家主持的深入细致的技术讲座并在专家的辅导下进行上机操作、了解高级技能和获得详细指导。在甲骨文开发者大会期间,您有机会直接向业界一流的技术专家和开发人员请教。欢迎参加甲骨文全球大会·2007·亚太地区开发者大会

2007年6月29日,自由软件基金会宣布,其创始人Richard Stallman将在GNU的网站上,在本周太平洋时间星期五上午9点通过视频发布GPLv3。本来,GPL并不是所有开源组织所认可的协议。其从出现以来一直存在争议,GPL被认为是一种“病毒式”的协议,BSD的fans和老牌Unix黑客们认为,他们编写Unix的年头都比GPL声明要长得多,他们更愿意采用比GPL更加的自由的BSD协议。今天,开源社区中有70%左右的项目采用了GPL。很多在开源社区的老牌黑客们认为,Richard Stallman所鼓吹开源软件的言行与当年卡尔·马克思号召产业无产阶级反抗工作的努力如出一辙。在GPLv3的第三版修订案发布时,开源软件团体中的许多成员都反对这种协议。尤其是Linux的核心开发小组,其中29个高级架构师有28个反对这个协议。Linus Torvalds称这个协议有“宗教性质”,并公开反对。而整个软件行业特别是开源社区对GPLv3的争论也愈演愈烈。GPLv3:大教堂和集市的新一轮对抗

2007年7月14日由CSDN与ThoughtWorks联合主办的第二届“敏捷中国”技术大会在北京丽亭华苑酒店召开,多位开源社区和ThoughtWorks公司的技术领袖即将带来精彩的演讲。本次“敏捷中国”技术大会集中展现塑造敏捷企业所需的方方面面:业界领先的敏捷项目管理工具;极大提升软件开发效率的新语言和新框架;数据库领域的敏捷实践;全方位的敏捷项目管理指导;还有身临其境的亲身体验。来自开源社区和ThoughtWorks公司的技术领导者们将带领听众全面感受敏捷企业。“敏捷中国大会”现场直击

从2004年起,在每年的夏季,CSDN都会举办面向中国程序员的大型网上调查活动。这是中国样本最丰富的开发者社区调查,持续、全面和深入地反映了中国开发者社群自身状况、各项技术、工具、产品的使用状况和发展趋势,是完整、准确地了解中国开发者市场的重要参考资料。本次调查覆盖基础信息、.NET、Java、C/C++、Web开发、数据库应用开发、软件工程及项目管理、移动及嵌入式开发、开放源代码、企业信息化等10个领域。还有惊喜大奖等着你哦,赶快进入吧! 2007中国软件开发者大调查正式启动

推荐作者
  • 大宝大宝

    时间如流水,知惜方成功。

  • SkymanSkyman

    江苏人氏,梅兰芳之老乡。现游学渝州之最...

  • ralph623ralph623

新进作者
  • 冲 s冲 s

  • 小鱼小鱼

  • 棱角棱角

    多年J2EE构架设计与开发经验,专注于企业信息系统建设,精通Java设计模式,并能熟练的运用到企业开发中。 精通Struts与Spring框架。数据库方面精通Oracle数据库,从事过数据库方面的开发以及oracle优化方面的工作。

最新技术图书推荐
孔乙己之四----虚函数(中)

发表日期:2006-8-07
更新日期:2006-8-10
作者文章阅读次数:4114

源自:大宝 (个人网站) 标签:c/c++

您认为本文应该得        共有3人参与打分打印|收藏|讨论|投诉

虽然此文的标题是"虚函数", 但本文实际上是围绕着这样一个中心点展开诸多内容, 包括: 单继承, 多继承, 以及在两个类发生继承关系时, 他们的数据和函数又是如何处理的?

本文作者:sodme
本文出处:http://blog.csdn.net/sodme
声明: 本文可以不经作者同意, 任意复制, 转载, 但任何对本文的引用都请保留文章开始前三行的作者, 出处以及声明信息. 谢谢.

听着窗外的大粒雨点声睡觉, 是一种特别的享受. 这两天恰逢台风登陆, 广州也连日的雨下个不停. 在这样的雨夜里, 收拾一下心情, 决定继续用ASM研究C++的对象模型. 写文章, 还是很需要好心情的. 心情不好的时候, 即使憋出了内容, 亦了无生趣. 而于我等作技术的人, 除了玩玩技术之外, 在文字方面给自己, 同时也多给读者一些阅读或写作的乐趣, 实在是一种明智之举. 所以, 我的习惯是: 只有心情好的时候, 才落笔. 
 
没错, 早在写这个孔乙己系列之前, 我就知道在C++对象模型的研究和介绍方面, 早就有了经典之作"Inside the C++ Object Model". 但是, 那毕竟是人家写的东西, 不是我亲自研究弄明白的东西. 之所以写作这个系列, 是想通过ASM分析这种方法, 对C++对象模型有个更全面, 更深刻, 也更感性的认识. 可以看看各种编译器在对C++标准支持方面是如何作的. 而在分析过程中, 对ASM分析方法的进一步熟悉, 则是意料之中的小小收获了. 至于这种黑箱分析所带来的乐趣, 则更是只能意会不能言传了.
 
虽然此文的标题是"虚函数", 但本文实际上是围绕着这样一个中心点展开诸多内容, 包括: 单继承, 多继承, 以及在两个类发生继承关系时, 他们的数据和函数又是如何处理的?
 
文章将按以下主线展开: 单继承下的虚函数机制分析, 多继承下的虚函数机制分析.
 
c++要实现虚函数, 归纳起来, 其实只用干两件事:
1. 根据派生和继承关系, 生成虚函数表;
2. 将代码中对虚函数的调用, 转化成对虚函数表中各虚函数指针的间接调用.
 
虽然在上文中的那个小例子中, 我们通过反汇编出来的asm弄明白了以下事实:
1. 每个"类"所拥有的虚函数表的个数: 每个类都会对应"唯一的一份"虚函数表;
2. "对象"中虚函数表指针的保存位置: 在调用对象的构造函数时, 会把虚函数表指针放到this处(即: 对象首地址处), 所以属于同一个类的每个对象的this地址所指向的单元处存放的是同一个值;
3. 虚函数的调用转化: 编译器编译c++代码时, 根据类的相关信息, 判断此函数是不是虚函数, 如果是, 则从虚函数表中取出真正的虚函数地址, 生成真正的调用语句;
 
那么, 现在看来, 问题的核心就变成了: 这个统领全局的虚函数表到底是如何产生? 在生成虚函数表的过程中, 遵守了哪些规则呢? 带着这些疑问, 我们将开始今天的探索. 
 
本文所需代码可从以下地址获得( 此地址含有单继承c++和asm代码 ):
http://sodme.dev.googlepages.com/kyj_04_code.txt
 
为了研究的方便, 此次对代码结构进行了较大规模的调整, 主要是:
1. 去除了对输入输出流的引用, 只保存逻辑代码, 不再include任何多余代码;
2. 声明了两个基类, 它们在单继承和多继承方式下会被拿来作不同的整合: 在单继承下, Base2Class派生于Base1Class, 而MyClass类派生于Base2Class类; 而在多继承下, 两个基类相互之间不再有派生关系, 它们会被MyClass一个类多继承.
 
先看单继承的情况. 三个类的派生关系如下:
Base1Class -> Base2Class -> MyClass, 其派生规则为: 多层单继承.
 
其中, 
 
Base1Class的虚函数表为:

    .long    _ZN10Base1Class14virtual_test_1Ev            ;Base1Class::virtual_test_1()
    .
long    _ZN10Base1Class14virtual_test_3Ev            ;Base1Class::virtual_test_3() 

 
Base2Class的虚函数表为:
    .long    _ZN10Base1Class14virtual_test_1Ev            ;Base1Class::virtual_test_1()
    .
long    _ZN10Base2Class14virtual_test_3Ev            ;Base2Class::virtual_test_3()
    .
long    _ZN10Base2Class14virtual_test_2Ev            ;Base2Class::virtual_test_2() 

 
MyClass的虚函数表为:
    .long    _ZN7MyClass14virtual_test_1Ev                ;MyClass::virtual_test_1()
    .
long    _ZN10Base2Class14virtual_test_3Ev            ;Base2Class::virtual_test_3()
    .
long    _ZN10Base2Class14virtual_test_2Ev            ;Base2Class::virtual_test_2()
    .
long    _ZN7MyClass15virtual_test_myEv                ;MyClass::virtual_test_my() 

 
由这三个虚函数表, 我们发现了以下的规律:
 
1. 派生类会继承基类的所有非同名虚函数, 存放顺序同基类;
如: 在Base2Class中, virtual_test_1() 是 Base1Class的虚函数, 且在Base2Class中没有定义同名函数, 它的顺序是1, 因为在基类Base1Class中, 它的顺序就是1;
 
2. 当派生类与基类有同名虚函数时, 派生类的虚函数表中存放派生类的虚函数, 存放顺序与基类同名虚函数的存放位置相同;
如: 在Base2Class中, virtual_test_3() 在 Base1Class和Base2Class中都有, 那么在Base2Class的虚函数表中, 就只会存放Base2Class自己的虚函数, 但存放的顺序, 与它的基类Base2Class的存放顺序相同! 而"存放顺序必须相同", 这一点, 是C++实现动态重载的重要方法! 这一点, 后面分析构造函数运行流程时会说到.
 
3. 存放完所有与基类同名的虚函数 以及 基类有但派生类没有的所有虚函数后, 开始存放派生类的其它虚函数.
如: 在Base2Class的virtual_test_2() 和 MyClass的virtual_test_my(), 这两个函数, 都是派生类中存在而基类中不存在的, 它们存放的顺序都在上述两类虚函数的后面.
 
以上, 是关于单继承情况下虚函数表的静态结构分析. 那么, 我们来看看实际运行时, 构造函数又是如何利用这些数据将对象进行初始化的.
 
MyClass构造函数的主要语句:
 
    movl    8(%ebp), %eax               ;申请的空间首地址, 即this指针
    movl    
%eax, (%esp)
    call    _ZN10Base2ClassC2Ev         ;调用Base2Class的构造函数
    movl    $_ZTV7MyClass
+8%edx       ;将MyClass的虚函数表地址放在edx中, 以便放在this处
    movl    
8(%ebp), %eax
    movl    
%edx, (%eax)                ;将虚表地址放在this处
    movl    
8(%ebp), %eax
    movl    $
112(%eax)                ;[this+12= 1 <==> data1 = 1
    movl    
8(%ebp), %eax
    movl    $
216(%eax)                ;[this+16= 2 <==> data2 = 2 

 
可以看到, 在MyClass的构造函数中, 首先是调用了Base2Class的构造函数: __ZN10Base2ClassC2Ev.
 
Base2Class的构造函数:
    movl    8(%ebp), %eax                ;this指针
    movl    
%eax, (%esp)
    call    _ZN10Base1ClassC2Ev          ;调用Base1Class的构造函数
    movl    $_ZTV10Base2Class
+8%edx    ;将Base2Class虚表地址放在this处
    movl    
8(%ebp), %eax
    movl    
%edx, (%eax)
    movl    
8(%ebp), %eax
    movl    $
448(%eax)                 ;[this+8= 44  <==>  base_2_data = 44  

 
而在Base2Class中, 又调用了Base1Class的构造函数:
    movl    $_ZTV10Base1Class+8%eax   ;将Base1Class虚表地址放在this处
    movl    
8(%ebp), %edx
    movl    
%eax, (%edx)
    movl    
8(%ebp), %eax
    movl    $
334(%eax)                ;[this+4= 33  <==>  base_1_data = 33 

 
从这三个构造函数的执行过程来看, 在Base1Class的构造函数中, 将Base1Class的虚函数表地址传到了this地址处, 在Base2Class的构造函数中, 将Base2Class的虚函数表地址也传到了this地址处, 同样的, 在MyClass的构造函数中, 把MyClass的虚函数表地址还是放在this地址处. 换句话说, 针对于同一个this地址处的这个虚函数表地址的赋值, C++竟然重复作了三次! 而最后一次的赋值, 才是我们真正需要的结果! 没错, C++在这里, 确实是作了两次"无用功". 我在想, 有没有办法避免这样的重复赋值呢? 有一种方法: 在调用构造函数时, 传个参数进来, 表示是不是派生类调用它的, 由个参数来决定是不是将虚函数表地址拿到this, 但是, 仔细算一下: "传递参数+判断" 这样的逻辑, 似乎还没有不管三七二十一直接赋值来得更为简单一些, 所以, 这样看来, C++选择这样作, 也是有充分理由的.
 
前面反复提到过虚函数的存放顺序. 那么, 这个顺序, 与C++的动态重载到底有多大关系呢? 我们先看这条语句:
pMyClass->virtual_test_2();
 
C++在分析这条语句时, 首先会判断pMyClass是哪种类型, 此例中是MyClass, 然后判断它调用的函数是不是虚函数, 如果是虚函数, 则将调用形式转化为针对于虚函数表的间接调用.
 
而前面提到, 派生类中存在且在基类中也存在的虚函数, 在派生类虚函数表中的位置与基类位置相同. 所以, 不管是MyClass还是Base2Class的对象, 它们调用virtual_test_2()的汇编代码都是相同的:
 
pBaseClass->virtual_test_2():
 
    movl    -12(%ebp), %eax     ; -12(%ebp)表示局部变量pBaseClass
    movl    (
%eax), %eax        ; 取虚表
    addl    $
8%eax            ; 取virtual_test_2()存放地址
    movl    (
%eax), %edx        ; 取virtual_test_2()地址
    movl    
-12(%ebp), %eax
    movl    
%eax, (%esp)        ; 将this指针放栈顶传递
    call    
*%edx 

 
pMyClass->virtual_test_2():
 
    movl    -16(%ebp), %eax        ; -16(%ebp)表示局部变量pMyClass
    movl    (
%eax), %eax
    addl    $
8%eax
    movl    (
%eax), %edx
    movl    
-16(%ebp), %eax
    movl    
%eax, (%esp)
    call    
*%edx 

 
经过比对, 除了取this指针的局部变量地址不同, 其余代码均相同, 取虚函数地址时, 都是在this指针的基础上加8. 之所以可以作到这样, 就是因为有这个约定: 针对于同名函数, 它们在虚函数表中的位置相同. 当然, 这个约定并不一定要所有的编译器都来遵守, 针对于同一个编译器而言, 有一套约定就行了, 关键是要有约定, 而具体如何约定倒各有各的作法.
 
除此之外, 我们还发现, 在Base2Class类的virtual_test_2()函数中, 对于类Base2Class 数据成员base_2_data的访问, 已经被修正为this+8. 这是因为, 经过一系列的派生后, pMyClass对象结构变成如下的方式:
 
            |                             |
            |-----------------------------|
this ->     |    类MyClass虚函数表地址    |        0
            |-----------------------------|
            | Base1Class::base_1_data     |        +4
            |-----------------------------|
            | Base2Class::base_2_data     |        +8
            |-----------------------------|
            |     MyClass::data1          |        +12
            |-----------------------------|
            |     MyClass::data2          |        +16
            |-----------------------------|
            |                             |
 
其规则是: this地址处存放最上层类的虚函数表地址, 然后向下依次存放各基类的数据成员, 其顺序按类中的声明顺序排列. 需要指出的是, 这里存放的数据, 不仅包括public的数据, 也同时包括private的数据. 道理很简单: 在调用某层类的函数时, 该函数中仍有可能要引用到私有数据, 尽管此私有数据不能被外部函数访问. 所以, 从这个层面上来说, 私有数据仅仅是C++自己的"君子"约定, 我们完全使用比较流氓的方法来访问它. 比如我们在类Base1Class增加一private数据, 修改为如下形式:
class Base1Class
{
public:
    Base1Class()
{ base_1_data = 33; };
    
~Base1Class(){};
    
int base_1_data;
    
virtual void virtual_test_1(){ base_1_data = 10; };
    
virtual void virtual_test_3(){ base_1_data = 11; };
private:
    
int pri_base_1_data;
}

 
pri_base_1_data会被放在base_1_data和base_2_data之间. 如果要访问pri_base_1_data, 在取得this指针后, 我们就可以通过this+8来访问, 具体的c++代码可以是这样:
*( (char*)pMyClass + 8 ) = (int)3333;
 
那么, C++如何区别这两条语句呢? 
 
pBase2Class->virtual_test_2():
 
    movl    -12(%ebp), %eax
    movl    (
%eax), %eax
    addl    $
8%eax
    movl    (
%eax), %edx
    movl    
-12(%ebp), %eax
    movl    
%eax, (%esp)
    call    
*%edx 

 
((MyClass*)pBase2Class)->virtual_test_2():
 
    movl    -12(%ebp), %eax
    movl    (
%eax), %eax
    addl    $
8%eax
    movl    (
%eax), %edx
    movl    
-12(%ebp), %eax
    movl    
%eax, (%esp)
    call    
*%edx 

 
pMyClass->virtual_test_2():
 
    movl    -16(%ebp), %eax
    movl    (
%eax), %eax
    addl    $
8%eax
    movl    (
%eax), %edx
    movl    
-16(%ebp), %eax
    movl    
%eax, (%esp)
    call    
*%edx 

 
((Base2Class*)pMyClass)->virtual_test_2():
     
    movl    -16(%ebp), %eax
    movl    (
%eax), %eax
    addl    $
8%eax
    movl    (
%eax), %edx
    movl    
-16(%ebp), %eax
    movl    
%eax, (%esp)
    call    
*%edx 

 
比较完这四条语句的代码之后, 我们发现: 不管作不作类型转换, 其编译出的汇编代码全是一样的!  其实, 前面的类型转换只是后面是否可以调用某函数的类型约定, 至于此函数的具体执行行为, 则完全看对象本身的虚函数表是哪份以及this指针指向的数据是什么而定了, this指针指向的虚函数表决定了函数的最终行为归属, 而此表地址在对象初始化时被放在了对象首地址处, 这样想来, 好像前后的知识可以贯通了:
对于一个pBase2Class而言, 我如何知道形如这样的调用:
pBase2Class->virtual_test_2(); 
它调用的到底是类Base2Class的, 还是MyClass的? 一切答案皆在虚函数表中. 对象与"某个类的虚函数表"绑定, 而virtual_test_2()则与"虚函数表地址+8"绑定, 要想知道调用的是哪个, 就看此时的this到底是哪个类的this.  
 
也就是说, 在对virtual_test_2()此函数的调用上, 不管是Base2Class类的对象还是MyClass类的对象, 其调用的汇编代码完全一样, 唯一可以让它们有不同执行行为的, 就是"addl $8, &eax"后取出的虚函数地址. 比如:
pBase2Class = new Base2Class; 则:
((MyClass*)pBase2Class)->virtual_test_my() 必定是非法的, 它会被转化成以下语句:
 
    movl    -12(%ebp), %eax
    movl    (
%eax), %eax
    addl    $
12%eax      ;根据类型为MyClass及函数为virtual_test_my()转化为"虚函数表地址+12"
    movl    (
%eax), %edx
    movl    
-12(%ebp), %eax
    movl    
%eax, (%esp)
    call    
*%edx 

 
由此, 可以看出, 编译器会根据当前对象的类型, 决定如何编译virtual_test_my(), 这里的"类型", 指的是"调用时类型", 而不是简单的指"定义时类型", 因为调用时, 可能会进行强制类型转换. 这里, 尽管virtual_test_my()函数可以编译通过, 但通过"this->虚函数表+12"找出来的这个地址, 根据不是正常的函数地址, 此时this指向实际对象所拥有的虚函数表中最多只有2个虚函数(+8).
 
 
 
 

您认为本文应该得        共有3人参与打分打印|收藏|讨论|投诉

暂无图片

时间如流水,知惜方成功。

评论

CSDN技术中心团队官方Blog:http://blog.csdn.net/techcenter/,反馈邮箱:techcenter at csdn.net (注意:请把 at 换成@)


网站简介广告服务网站地图帮助联系方式诚聘英才English问题报告

北京百联美达美数码科技有限公司  版权所有  京 ICP 证 020026 号

Copyright © 2000-2006, CSDN.NET, All Rights Reserved