深度探索C++对象模型 Inside The C++ Object Model中文版

奔跑的面包

贡献于2014-10-15

字数:0 关键词: C/C++开发 C/C++

GOTOP 深度探索 ++ 对象模型 C Inside The C++ Object Model Stanley B. Lippman 着 侯捷 译 •Object Lessons •The Semantics of Constructors •The Semantics of Data •The Semantics of Function •Semantics of Construction, Destruction, and Copy •Runtime Semantics •On the Cusp of the Object Model 深度探索 C++ 对象模型 Inside The C++ Object Model Stanley B. Lippman 着 侯捷 译 碁峰信息股份有限公司 本立道生(译序) 本立道生 (侯捷 译序) 对于传统的循序性 (sequential)语言,我们向来没有 太多的疑惑 ,虽然在函式呼 叫的背后,也有着堆栈建制、参数排列、回返地址 、堆栈清除等等 幕后机制,但 函式呼叫是那么 ㆞自然而明显 ,好像只是夹带着 ㆒个包裹,从程序的某 ㆒个㆞点 跳到另㆒个㆞点去执行。 但是对于对象 导向(Object Oriented )语言,我们的疑惑就多了 。究其因,这种语 言的编译器为我们(程序员)做了太多 的服务:建构式、解构式、虚拟函式、继 承、多型...。有时候它为我们合成 出㆒些额外的函式(或运算子),有时候它又 扩张我们所写的函式内容,放进更多的动作。有时候它还会为 我们的 objects 加 油添醋,放进㆒些奇妙的东 西,使你面对 sizeof 的结果大惊失色。 存在我心里头 ㆒直有个疑惑 :计算机程序最基础的形式,总是脱离不了 ㆒行㆒行的 循序执行模式,为什么 OO(对象导向 )语言却能够 「自动完成 」这么多事情呢 ? 另㆒个疑惑 是,威力强大的 polymorphism (多型),其底层机制究竟如何 ? 1 深度探索 C++ 对象模型(Inside The C++ Object Model ) 如果不了解编译器对我 们所写的 C++ 码做了什么 手脚,这些困惑永远解不开。 这本书解决 了过去令我百思不 解的诸多疑惑 。我要向 所有已具备 C++ 多年程序 设计经验的同 好们大力推荐 这本书。 这本书同时也是 跃向组件软件( component-ware)基本精神 的跳板。不管你想学习 COM(Component Object Model )或 CORBA (Common Object Request Broker Architecture )或是 SOM (System Object Model ),了解 C++ Object Model ,将使 你更清楚软件组件( components)设计㆖的难点与运应之道 。不但我自己在学习 COM 的道路 ㆖有此强烈的感受 ,Essential COM(COM 本质论,侯捷译,碁峰 1998 )的作者 Don Box 也在他的书 ㆗推崇 Lippman 的这㆒本卓越的书籍。 是的,这当然不会是 ㆒本轻松的书籍。某些章节(例如3、4两章)可能给你立 即的享受 -- 享受于面对底层机 制有所体会与掌控的快乐;某些章节(例如5、 6、7㆔章)可能带给你短暂的痛苦 -- 痛苦于艰难深涩 难以吞咽的内容。这些 快乐与痛苦,其实 就是我翻译此书时 的心情写照。无论如 何,我希望透过 我的译 笔,把这本难得的好书带 到更多 ㆟面前,引领大家见识 C++ 底层建 设的技术之 美。 侯捷 1998.05.20 于新竹 jjhou@ccca. nctu. edu. tw 2 本立道生(译序) 请注意:本书属 性,作者 Lippman 在其前言 ㆗有很详细 的描述,我不再 多言。翻 译用词与心得,记录在第0章(译者的话)之 ㆗,对您或有导读之 功。 请注意:原文 本有大大小小 约 80~90 个笔误。有的无伤大雅,有的影响阅 读顺畅 甚巨(如前后文所用 符号不㆒致、内文与图 形所用符号不㆒致 -- 甚至因而导至 图片的文字 解释不正确)。我已在第0章(译者的话)列出所有我 找到的错误 。 此外,某些场合我还会在错误出现之处再加注 ,表示原文 内容为何。这么做不是 画蛇添足,也不为彰显什么 。我知道有些读 者拿着原文 书和㆗译书对照着看,我 把原书错误加注 出来,可免读者怀疑是否我打错字或是译错 了。另 ㆒方面也是为 了文责自负 ...唔...万㆒ Lippman 是对的而 J.J.Hou 错了呢 ?! 我虽有相当把握, 还是希望明白摊开来让读者检验。 3 深度探索 C++ 对象模型(Inside The C++ Object Model ) 4 目录 深度探索 C++ 对象模型 Inside The C++ Object Model 目录 本立道生(侯捷 译序) 目录 前 言(Stanley B. Lippman ) 第0章 第1章 导 读(译者的话) 关于物件( Object Lessons ) / 001 / 005 / 013 / 025 / 001 / 005 / 006 / 007 / 008 / 009 / 013 / 015 / 016 加㆖封装后的布局成 本(Layout Costs for Adding Encapsulation ) 1.1 C++ 对象模式(The C++ Object Model ) 简单对象模 型(A Simple Object Model ) 表格驱动对象模 型(A Table-driven Object Model ) C++ 对象模型(The C++ Object Model) 对象模型如何影响程序( How the Object Model Effects Programs ) 1.2 关键词所带来的差异( A Keyword Distinction ) 关键词的苦恼 5 深度探索 C++ 对象模型(Inside The C++ Object Model ) 策略性正确的 struct (The Politically Correct Struct ) 1.3 对象的差异( An Object Distinction ) 指标的型别 (The Type of a Pointer ) 加㆖多型之后( Adding Polymorphism ) 第2章 2.1 建构式语意学( The Semantics of Constructors ) Default Constructor 的建构动作 「带有 Default Constructor 」的 Member Class Object 「带有 Default Constructor 」的 Base Class 「带有㆒个 Virtual Function 」的 Class 「带有㆒个 Virtual Base Class 」的 Class 总结 2.2 Copy Constructor 的建构动作 Default Memberwise Initialization Bitwise Copy Semantics (位逐次拷贝 ) 不要 Bitwise Copy Semantics ! 重新设定 Virtual Table 的指标 处理 Virtual Base Class Subobject 2.3 程序转换语意学( Program Transformation Semantics ) 明显的初始化动作( Explicit Initialization ) 参数的初始化( Argument Initialization ) 回返值的初始化( Return Value Initialization ) 在使用者层面做 最佳化( Optimization at the User Level ) 在编译器层面 最佳化(Optimization at the Compiler Level ) Copy Constructor :要还是不要 ? / 019 / 022 / 028 / 029 / 037 / 039 / 041 / 044 / 044 / 046 / 047 / 048 / 049 / 051 / 053 / 054 / 057 / 060 / 061 / 062 / 063 / 065 / 066 / 072 6 目录 摘要 2.4 第3章 成员们的初始化队伍( Member Initialization List ) Data 语意学( The Semantics of Data ) / 074 / 074 / 083 / 088 / 092 / 094 / 095 / 097 / 099 / 100 / 107 / 112 / 116 / 124 / 129 / 134 / 139 / 140 / 141 / 147 / 148 / 152 / 159 3.1 Data Member 的系结(The Binding of a Data Member ) 3.2 Data Member 的布局( Data Member Layout ) 3.3 Data Member 的存取 Static Data Members Nonstatic Data Members 3.4 「继承」与 Data Member 只要继承不要多型( Inheritance without Polymorphism ) 加㆖多型( Adding Polymorphism ) 多重继承(Multiple Inheritance ) 虚拟继承(Virtual Inheritance ) 3.5 对象成员的效率( Object Member Efficiency ) 3.6 指向 Data Members 的指标(Pointer to Data Members ) 「指向 Members 的指标」的效率问题 第4章 Function 语意学( The Semantics of Function ) 4.1 Member 的各种唤起方式( Varieties of Member Invocation ) Nonstatic Member Functions (非静态虚拟函式 ) Virtual Member Functions (虚拟成员函式 ) Static Member Functions (静态成员函式 ) 4.2 Virtual Member Functions (虚拟成员函式 ) 多重继承㆘的 Virtual Functions 7 深度探索 C++ 对象模型(Inside The C++ Object Model ) 虚拟继承㆘的 Virtual Functions 4.3 函式的效能 4.4 指向 Member Function 的指标 (Pointer-to-Member Functions ) 支援「指向 Virtual Member Functions 」之指标 在多重继承 之㆘,指向 Member Functions 的指标 「指向 Member Functions 之指标 」的效率 4.5 Inline Functions 形式参数( Formal Arguments ) 区域变量 / 168 / 170 / 174 / 176 / 178 / 180 / 182 / 185 / 186 第5章 建构、解构、拷贝 语意学( Semantics of Construction, Destruction, and Copy ) / 191 纯虚拟函式的存在 (Presence of a Pure Virtual Function ) 虚拟规格的 存在(Presence of a Virtual Specification ) / 193 / 194 虚拟规格㆗ const 的存在(Presence of const within a Virtual Spec ) / 195 重新考虑 class 的宣告 / 195 5.1 无继承情况㆘的对象建构(Object Constrtuction without Inheritance ) / 196 抽象数据型别 (Abstract Data Type ) 为继承做准备 5.2 继承体系㆘的对象建构 虚拟继承(Virtual Inheritance ) vptr 初始化语意学( The Semantics of the vptr Initialization ) 5.3 对象复制语意学( Object Copy Semantics ) 5.4 物件的效能( Object Efficiency ) 5.5 解构语意学( Semantics of Destruction ) / 198 / 202 / 206 / 210 / 213 / 219 / 225 / 231 8 目录 第6章 执行时期语意学(Runtime Semantics ) / 237 / 240 / 242 / 247 / 250 / 252 / 254 / 257 / 263 / 267 / 275 / 279 / 280 / 281 / 285 / 289 / 292 / 297 / 298 / 303 / 308 / 310 / 311 6.1 物件的建构和解构(Object Construction and Destruction ) 全域物件(Global Objects ) 区域静态对象(Local Static Objects ) 对象数组(Array of Objects ) Default Constructors 和数组 6.2 new 和 delete 运算子 对于数组的 new 语意 Placement Operator new 的语意 6.3 暂时性对象 暂时性对象的迷思(神话、传说) 站在对象模型的尖端(On the Cusp of the Object Model ) 第7章 7.1 Template Template 的「具现」行为(Template Instantiation ) Template 的错误告发(Error Reporting within a Template ) Template ㆗的名称决议法(Name Resolution within a Template ) Member Function 的具现行为(Member Function Instantiation ) 7.2 Exception Handling (异常处理) Exception Handling 快速检阅 对 Exception Handling 的支援 7.3 执行时期型别辨识(Runtime Type Identification ,RTTI) Type-Safe Downcast (保证安全的向㆘转型动作) Type-Safe Dynamic Cast (保证安全的动态转型) 9 深度探索 C++ 对象模型(Inside The C++ Object Model ) References 并不是 Pointers Typeid 运算子 7.4 效率有了,弹性呢? 动态共享函式库( Dynamic Shared Libraries ) 共享内存( Shared Memory ) / 313 / 314 / 318 / 318 / 318 10 目录 11 深度探索 C++ 对象模型(Inside The C++ Object Model ) 12 前言 前言 (Stanley B. Lippman ) 差不多有 10 年之久,我在贝尔实验室 (Bell Laboratories )埋首于 C++ 的实作 任务。最初的工作是在 cfront ㆖面(Bjarne Stroustrup 的第㆒个 C++ 编译器), 从 1986 年的 1.1 版到 1991 年九月的 3.0 版。然后 移转到 Simplifier (这是我 们内部的命名 ) ,也就是 Foundation 项目㆗的 C++ 对象模型部份。在 Simplifier 设计期间,我开始酝酿这本书。 Foundation 项目是什么 ?在 Bjarne 的领导 ㆘,贝尔实验室 ㆗的㆒个小组探索着 以 C++ 完成大规模程序设计时的种种 问题的解决之道 。Foundation 项目是我们 为了建构大系统而努力定义的 ㆒个新的开发模 型;我们只使用 C++ ,并不提供多 重语言的解决 方案。这是个令 ㆟兴奋的工作, ㆒方面是因为工作本 身,㆒方面是 因为工作伙伴: Bjarne、Andy Koenig 、Rob Murray 、Martin Carroll 、Judy Ward 、 Steve Buroff 、Peter Juhl 、以及我自己 。Barbara Moo 管理我们这 ㆒群㆟(Bjarne 和 Andy 除外)。Barbara Moo 常说管理㆒个软件团队,就像放牧 ㆒群骄傲的猫。 13 深度探索 C++ 对象模型(Inside The C++ Object Model ) 我们把 Foundation 想象是 ㆒个核心,在那㆖面,其它 ㆟可以为使用者铺设 ㆒层真 正的开发环境,把它整修为他们所期望的 UNIX 或 Smalltalk 模型。私底 ㆘我们 把它称为 Grail (传说㆗耶稣最后晚餐所用的圣杯 ),㆟㆟都想要 ,但是从来没 ㆟ 找到过! Grail 使用㆒个由 Rob Murray 发展出来并命名 为 ALF 的对象导向阶层架构,提 供㆒个永久的、以语 意为基础的表现 法。在 Grail ㆗,传统编译器被分解为数个 各自分离的可执 行档。parser 负责建立程序的 ALF 表现法。其它每 ㆒个组件(像 是 type checking 、simplification 、code generation )以及工具 (像是 browser )都在 程序的㆒个 ALF 表现体㆖操作(并可能加以膨胀 )。Simplifier 是编译器的 ㆒部 份,处于 type checking 和 code generation 之间。Simplifier 这个名称是由 Bjarne 所倡议,它原本是 cfront 的㆒个阶段(phase)。 在 type checking 和 code generation 之间,Simplifier 做什么 事呢?它用来转换内 部的程序表现 。有㆔种转换风味是任何对象模 型都需要的: 1. 与编译器息息 相关的转 换(Implementation-dependent transformations ) 这是与特定编译器有关的转 换。在 ALF 之㆘,这意味着我们所谓的 "tentative" nodes 。例如,当 parser 看到这个表达式: fct(); 它并不知道是否 (a) 这是㆒个函式唤起动作,或者 (b) 这是 overloaded call operator 在 class object fct ㆖的㆒种应用。预设情况 ㆘,这个式子所 代表的是㆒个 函式呼叫,但是当 (b) 的情况出现, Simplifier 就要重写并调换 call subtree 。 2. 语言语意转换( Language semantics transformations ) 这 包 括 constructor/destructor 的 合 成 和 扩 张 、 memberwise 初 始 化 、 对 于 memberwise copy 的支持、在程序代码 ㆗安插 conversion operators 、暂时性对象 、以 及对 constructor/destructor 的呼叫。 14 前言 3. 程序代码和对象模 型的转换(Code and object model transformations ) 这包括对 virtual functions 、virtual base class 和 inheritance 的㆒般支持、new 和 delete 运算子、class objects 所组成的数组、 local static class instances 、带有非常数 表达式 (nonconstant expression ) global object 的静态初始化动作 我对 Simplifier 之 。 所规划的㆒个目标是 :提供㆒个对象模 型体系 ,在其 ㆗,对象的实作是 ㆒个虚拟 接口,支持各种对象模 型。 最后两种类型 的转换构成了本书的基础。这意味本书是为编译器设计者而写的 吗?不是,绝对不是!这本书是由 ㆒位编译器设计者针对 ㆗高阶 C++ 程序员所 写。隐藏在这本书背后的假设是,程序员如果了解 C++ 对象模型,就可以写出 比较没有错 误倾向而且比 较有效率的码。 什么是 C++ 对象模型 有两个概念可以解释 C++ 对象模 型: 1. 语言㆗直接支持对象 导向程序设计的部份。 2. 对于各种支持的底层实作机制。 语言层面的支持,涵盖于 我的 C++ Primer ㆒书以及其它许多 C++ 书籍当㆗。 至于第㆓个概念,则几乎不能 够于目 前任何读物 ㆗发现,只有 [ELLIS90] 和 [STROUP94] 勉强有㆒些蛛丝马迹。本书主要专注于 C++ 对象模型的第㆓个概 念。本书语言遵循 C++ 委员会于 1995 冬季会议 ㆗通过的 Standard C++ 草案 (除了某些细节,这份草案应该能 够反映出此语言的最终版 本)。 C++ 对象模型的第㆒个概念是㆒种「不变量」。例如, C++ class 的完整 virtual functions 在编译时期就固定 ㆘来了,程序员没有 办法在执行时期动态增加或取 代 其㆗某㆒个。这使得 虚拟唤起动作得有快速的派送( dispatch)结果,付出的成本 则是执行时期的弹性。 15 深度探索 C++ 对象模型(Inside The C++ Object Model ) 对象模型的底层实作机制,在语言层面 ㆖是看不出来的 -- 虽然对象模 型的语意 本身可以使得 某些实作品(编译器)比其它实作品更接近自然。例如, virtual function calls ,㆒般而言是藉由 ㆒个表格(内含 virtual functions 地址)的索引而 决议得知。 ㆒定要使用如此的 virtual table 吗?不,编译器可以自由引进其它任 何变通作法。如果使用 virtual table ,那么其布局 、存取方法 、产生时机 、以及数 百个细节也都必 须决定㆘来,而所有决定也都由每 ㆒个实作品(编译器)自行取 舍。不过,既然说到这里,我也必须明白告诉你,目前所有编译器对于 virtual function 的实作法都 是使用各个 class 专属的 virtual table ,大小固定,并且在程 式执行前就建构好了。 如果 C++ 对象模型的底层机 制并未标准化,那么你 可能会问:何必 探讨它呢? 主要的理由是,我的经验告诉我 ,如果㆒个程序员了解底层实作模型,他就能够 写出效率较高的码,自信心也比较高。 ㆒个㆟不应该用猜的 ,或是等待某大师的 宣判,才确定「何时提供 ㆒个 copy constructor 而何时不需要」。这类问题的解答 应该来自于我们自身对对象模 型的了解。 写这本书的第 ㆓个理由是为了消除我们对于 C++ 语言(及其对对象 导向的支 援) 的各种错误认知。 ㆘面㆒段话节录自我收到的 ㆒封信,来信者希望将 C++ 引 进其程序环 境㆗: 我和㆒群㆟工作,他们过去不曾写过(或完全不熟悉) C++ 和 OO。其㆗㆒位工 程师从 1985 就开始写 C 了,他非 常强烈㆞认为 C++ 只对那些 user-type 程序 才好用,对 server 程序却不理想。他说如果要写 ㆒个快速而有效率的数据库引 擎,应该使用 C 而非 C++ 。他认为 C++ 庞大又迟缓。 C++ 当然并不是㆝生㆞庞大又迟缓,但我发 现这似乎成为 C 程序员的 ㆒个共 识。然而,光是这么说并不足以使 ㆟信服,何况 我又被认为是 C++ 的 「代言㆟」 。 这本书就是企图极尽可能 ㆞将各式各样的 Object facilities (如 inheritance 、virtual functions 、指向 class members 的指标…)所带来的额外负荷 说个清楚。 16 前言 除了我个㆟回答这封信 ,我也把此信转寄 给 HP 的 Steve Vinoski ;先前我曾与他 讨论过 C++ 的效率问题。以 ㆘节录自他的回应: 过去数年我听过太多 与你的同事类似的看法。许多情况 ㆘,这些看 法是源于对 C++ 事实真象的缺乏了解。就在 ㆖周,我才和 ㆒位朋友闲聊,他在 ㆒家 IC 制造 厂服务,他说他们不使用 C++ ,因为 「它在你的背后做事情 」。我紧迫盯 ㆟,于 是他说根据他 的了解,C++ 呼叫 malloc() 和 free() 而不让程序员知道。这当然 不是真的。这是㆒种所谓的迷思与传说,引导出类似于你的同 事的看法 ... 在抽象性和实际性之间找出平衡点 ,需要知识、经验、以及许多思考。 C++ 的使 用需要付出许多心力 ,但是我的经验告诉我 ,这项投资的报酬率相当高。 我喜欢把这本书想象是我对 那㆒封读者来信的回答。是的,这本书是 ㆒个知识陈 列库,帮助大家去除围 绕在 C++ ㆕周的迷思与传说。 如果 C++ 对象模型的底层机 制会因为实作品(编译器)和时间的 变动而不同, 我 如 何 能 够 对 于 任 何 特 定 主 题 提 供 ㆒ 般 化 的 讨 论 呢 ? 静 态 初 始 化 ( Static initialization )可为此提供 ㆒个有趣的例子 。 已知㆒个 class X 有着 constructor ,像这样: class X { friend istream& operator>>( istream&, X& ); public: X( int sz = 1024 ) { ptr = new char[ sz ]; } ... private: char *ptr; }; 而㆒个 class X 的 global object 的宣告,像这样: 17 深度探索 C++ 对象模型(Inside The C++ Object Model ) X buf; main() { // buf 必须在这个时候建构起来 cin >> setw( 1024 ) >> buf; ... } C++ 对象模型保证,X constructor 将在 main() 之前便把 buf 初始化。然而它并 没有说明这是如何办到的。答案是所谓的静态 初始化( static initialization ),实际 作法则有赖开发环境对此的 支持属于 哪㆒层级。 原始的 cfront 实作品不单 只是假 想没有环境支持,它也假 想没有 明白的目标平 台。唯㆒能够假想的平台就是 UNIX 及其衍化的 ㆒些变体。我们的 解决之道也因 此只专注在 UNIX 身㆖:我们使用 nm 命令。CC 命令(㆒个 UNIX shell script ) 产生出㆒个可执行档,然后 我们把 nm 施行于其㆖,产生 出㆒个新的 .c 档案。 然后编译此㆒新的 .c 档,再重 新联结出㆒个可执行档(这就 是所谓的 munch solution )。这种作法是以编译器时间来交换移植性 。 接㆘来是提供 ㆒个「平台特定」解决之道 :直接验证并穿越 COFF-based 程序的 可执行档(此即所谓的 patch solution ) ,不再需要 nm 、compile 、relink。COFF 是 Common Object File Format 的缩写,是 System V pre-Release 4 UNIX 系统所发展 出来的格式。这两种解决 方案都属于 程序层面 ,也就是说,针对每 ㆒个需要静态 初始化的 .c 檔,cfront 会产生 出㆒个 sti 函式,执行必要的初始化动作。不论是 patch solution 或是 munch solution ,都会去寻找以 sti 开头的 函式,并且安排它 们以㆒种 未被定 义的次 序执行起 来(藉 由安插 在 main() 之 后第㆒ 行的㆒ 个 library function _main() 执行之)(译注:本书第6章对此有详细 说明)。 System V COFF-specific C++ 编译器与 cfront 的各个版本平行发展。由于瞄准了 ㆒个特定平台和特定操作系统,此编译器因 而能够影响联结器特别为它修改:产 生出㆒个新的 .ini section ,用以收集需要静态初始化的 objects 。联结器的这种扩 18 前言 充方式,提供了所谓的 environment-based solution ,那当然更在 program-based solution 层次之 ㆖。 至此,任何以 cfront program-based solution 为基础的㆒般化(泛型 )动作将令㆟ 迷 惑 。 为 什 么 ? 因 为 C++ 已 经 成 为 主 流 语 言 , 它 已 经 接 收 了 更 多 更 多 的 environment-based solutions 。这本书如何维护其间的平衡呢 ?我的策略如 ㆘:如果 在不同的 C++ 编译器㆖有重大的实作技术差异,我就 讨论至少两家作法。但如 果 cfront 之后的编译器实作模型只是解决 cfront 原本就已理解的问题,例如对 虚拟继承的支持,那么我就 阐述历史的演化。当我说到「传统模型」,我的意思 是 Stroustrup 的原始构想(反应在 cfront 身㆖),它提供 ㆒种实作模范,在今 ㆝ 所有的商业化实 作品㆖仍然可见。 本书组织 第1章,关于对象 (Object Lessons ),提供以对象为基础的观念背景 ,以及 由 C++ 提供的对象 导向程序设计典范 (paradigm。译注:关于 paradigm 这个字, 请参阅本书 #22 页的译注)。本章包括对对象模 型的㆒个大略游览 ,说明目前普 及的工业产品,但没有对 于多重继承 和虚拟继承 有太靠近的观察(那是第3章和 第4章的重头戏)。 第2章,建构式语意学( The Semantics of Constructors ),详细讨论 constructor 如何工 作。本章谈到 constructors 何时被编译器合成 ,以及对你的程 式效率带来什么 样的意义。 第3章至第5章是本书的重要题材 。在这里,我详细 ㆞讨论了 C++ 对象模型的 细节。第3章, Data 语意学 (The Semantics of Data ) ,讨论 data members 的处理。第4章, Function 语意学 (The Semantics of Functi on ),专 注 于 各 式 各 样 的 member functions , 并 特 别 详 细 ㆞ 讨 论 如 何 支 援 virtual functions 。第5章,建构 、解构、拷贝语意学 (Semantics of Construction, Destruction, and Copy ),讨论如何支持 class 模型,也讨论到 object 的生 19 深度探索 C++ 对象模型(Inside The C++ Object Model ) 命期。每㆒章都有测试程序以及测试数据。我们对效率的预 测,将拿来和实际结 果做㆒比较。 第6章,执行时期语意学( Runtime Se mantics ),检视执行时期的某些对象 模型行为。包括暂时对象 的生命及其死亡,以及对 new 运算子和 delete 运算子 的支援。 第7章,在对象模 型的尖端( On the Cusp of the Object Model ),专注于 exception handling 、template support 、runtime type identification 。 预定的读者 这本书可以扮演家庭教师的角色 ,不过它定位在 ㆗阶以㆖的 C++ 程序员,而非 C++ 新手。我尝试 提供足够的内容,使它能够被任 何有点 C++ 基础(例如读过 我的 C++ Primer 并有㆒些实际程序经 验)的 ㆟接受。理想的读者是,曾经有过 数年的 C++ 程序经 验,希望更了解「底层做 些什么 事」的 ㆟。书㆗某些部份甚 至对于 C++ 高手也具 吸引力,像是 暂时性对象 的产生,以及 named return value (NRV)最佳化的细节等等 。在与本书相同素材 的各个公开演讲场 合㆗,我已经 证实了这些材料的吸引力。 程序范例及其执行 本书的程序范例主 要有两个目的: 1. 为了提供书 ㆗所谈之 C++ 对象模 型各种概念之具体说明。 2. 提供测 试,以量测各种语 言性质之相对成本。 无论哪㆒种意图,都只是为了展现对象模 型。举例而言,虽然我在书 ㆗有大量的 举例,但我并非 建议㆒个真实的 3D graphic library 必须以虚拟继承 的方式来表现 ㆒个 3D 点(不过你可以在 [POKOR94] ㆗发现作者 Pokorny 的确这么做)。 20 前言 书㆗所有测试程序都在 ㆒部 SGI Indigo2xL ㆖编译执行,使用 SGI 5.2 UNIX 作 业系统㆗的 CC 和 NCC 编译器。 CC 是 cfront 3.0.1 版(它会产生 出 C 码,再 由㆒个 C 编译器重新编译为可执 行档)。NCC 是 Edison Design Group 的 C++ front-end 2.19 版,内含 ㆒个由 SGI 供应的 程序代码产生 器。至于时 间量测,是采 用 UNIX 的 timex 命令针对 ㆒千万次迭代测 试所得的平均值。 虽然在 xL 机器㆖使用这两个编译器,对读者而言可能觉得有些神秘,我却觉得 对此书的目的而 言,很好。不论是 cfront 或现在的 Edison Design Group's C++ front-end (Bjarne 称其为「cfront 的儿子」),都与平台无 关。它们是 ㆒种㆒般 化的编译器,被授权给 34 家以㆖的计算机制造商(其 ㆗包括 Gray 、SGI、Intel ) 和软件开发环境厂商(包括 Centerline 和 Novell ,后者是原先的 UNIX 软件实 验室)。效率的量测并非 为了对目前市面 ㆖各家编译系统做评比 ,而只是为了提 供 C++ 对象模型之各种特性的 ㆒个相对成本量测。至于商 业评比的 效率数据, 你可以在几 乎任何 ㆒本计算机杂 志的计算机产品检验报告 ㆗获得。 致谢 略 参考书目 [BALL92] Ball, Michael, "Inside Templates", C++ Report (September 1992) [BALL93a] Ball, Michael, "What Are These Things Called Templates", C++ Report (February 1993) [BALL93b] Ball, Michael, "Implementing Class Templates", C++ Report (September 1993) [BOOCH93] Booch, Grady and Michael Vilot, "Simplifying the Booch Components", C++ Report (June 1993) [BORL91] Borland Language Open Architecture Handbook, Borland International Inc., Scotts Valley, CA [BOX95] Box, Don, "Building C++ Components Using OLE2", C++ Report (March/April 1995) [BUDD91] Budd, Timothy, An Introduction to Object-Oriented Programming, Addison-Wesley Publishing Company, Reading, MA(1991) 21 深度探索 C++ 对象模型(Inside The C++ Object Model ) [BUDGE92] Budge, Kent G., James S. Peery, and Allen C. Robinson, "High Performance Scientific Computing Using C++", Usenix C++ Conference Proceedings, Portland, OR(1992) [BUDGE94] Budge, Kent G., James S. Peery, Allen C. Robinson, and Michael K. Wong, "Management of Class Temporaries in C++ Translation Systems", The Journal of C Language Translation (December 1994) [CARROLL93] Carroll, Martin, "Design of the USL Standard Components", C++ Report (June 1993) [CARROLL95] Carroll, Martin, and Margaret A. Ellis, "Designing and Coding Reusable C++, Addison-Wesley Publishing Company, Reading, MA(1995) [CHASE94] Chase, David, "Implementation of Exception Handling, Part 1", The Journal of C Language Translation (June 1994) [CLAM93a] Clamage, Stephen D., "Implementing New & Delete", C++ Report (May 1993) [CLAM93b] Clamage, Stephen D., "Beginnings & Endings", C++ Report (September 1993) [ELLIS90] Ellis, Margaret A. and Bjarne Stroustrup, The Annotated C++ Reference Manual, Addison-Wesley Publishing Company, Reading, MA(1990) [GOLD94] Goldstein, Theodore C. and Alan D. Sloane, "The Object Binary Interface - C++ Objects for Evolvable Shared Class Libraries", Usenix C++ Conference Proceedings, Cambridge, MA(1994) [HAM95] Hamilton, Jennifer, Robert Klarer, Mark Mendell, and Brian Thomson, "Using SOM with C++", C++ Report (July/August 1995) [HORST95] Horstmann, Cay S., "C++ Compiler Shootout", C++ Report (July/August 1995) [KOENIG90a] Koenig, Andrew and Stanley Lippman, "Optimizing Virtual Tables in C++ Release 2.0", C++ Report (March 1990) [KOENIG90b] Koenig, Andrew and Bjarne Stroustrup, "Exception Handling for C++ (Revised)", Usenix C++ Conference Proceedings (April 1990) [KOENIG93] Koenig, Andrew, "Combining C and C++", C++ Report (July/August 1993) [ISO-C++95] C++ International Standard, Draft (April 28, 1995) [LAJOIE94a] Lajoie, Josee, "Exception Handling: Supporting the Runtime Mechanism", C++ Report (March/April 1994) [LAJOIE94b] Lajoie, Josee, "Exception Handling: Behind the Scenes", C++ Report (June 1994) [LENKOV92] Lenkov, Dmitry, Don Cameron, Paul Faust, and Michey Mehta, "A Portable Implementation of C++ Exception Handling", Usenix C++ Conference Proceeding, Portland, OR(1992) [LEA93] Lea, Doug, "The GNU C++ Library", C++ Report (June 1993) [LIPP88] Lippman, Stanley and Bjarne Stroustrup, "Pointers to Class Members in C++", Implementor's Workshop, Usenix C++ Conference Proceedings (October 1988) [LIPP91a] Lippman, Stanley, "Touring Cfront", C++ Journal, Vol.1, No.3 (1991) [LIPP91b] Lippman, Stanley, "Touring Cfront: From Minutiae to Migraine", C++ Journal, Vol.1, No.4 (1991) 22 前言 [LIPP91c] Lippman, Stanley, C++ Primer, Addison-Wesley Publishing Company, Reading, MA(1991) [LIPP94a] Lippman, Stanley, "Default Constructor Synthesis", C++ Report (January 1994) [LIPP94b] Lippman, Stanley, "Applying The Copy Constructor, Part1: Synthesis", C++ Report (February 1994) [LIPP94c] Lippman, Stanley, "Applying The Copy Constructor, Part2", C++ Report (March/April 1994) [LIPP94d] Lippman, Stanley, "Objects and Datum", C++ Report (June 1994) [METAW94] MetaWare High C/C++ Language Reference Manual, Metaware Inc., Santa Crus, CA(1994) [MACRO92] Jones, David and Martin J. O'Riordan, The Microsoft Object Mapping, Microsoft Corporation, 1992 [MOWBRAY95] Mowbray, Thomas J. and Ron Zahavi, The Essential Corba, John Wiley & Sons, Inc. (1995) [NACK94] Nackman, Lee R., and John J. Barton Scientific and Engineering C++, An Introduction with Advanced Techniques and Examples, Addison-Wesley Publishing Company, Reading, MA(1994) [PALAY92] Palay, Andrew J., "C++ in a Changing Environment", Usenix C++ Conference Proceedings, Portland, OR(1992) [POKOR94] Pokorny, Cornel, Computer Graphics, Franklin, Beedle & Associates, Inc. (1994) [PUGH90] Pugh, William and Grant Weddell, "Two-directional Record Layout for Multiple Inheritance", ACM SIGPLAN '90 Conference, White Plains, New York(1990) [SCHMIDT94a] Schmidt, Douglas C., "A Domain Analysis of Network Daemon Design Dimensions", C++ Report (March/April 1994) [SCHMIDT94b] Schmidt, Douglas C., "A Case Study of C++ Design Evolution", C++ Report (July/August 1994) [SCHWARZ89] Schwarz, Jerry, "Initializing Static Variables in C++ Libraries", C++ Report (February 1989) [STROUP82] Stroustrup, Bjarne, "Adding Classes to C: An Exercise in Language Evolution", Software: Practices & Experience, Vol.13 (1983) [STROUP94] Stroustrup, Bjarne, "The Design and Evolution of C++", Addison-Wesley Publishing Company, Reading, MA(1994) [SUN94a] The C++ Application Binary Interface, SunPro, Sun Microsystems, Inc. [SUN94b] The C++ Application Binary Interface Rationale, SunPro, Sun Microsystems, Inc. [VELD95] Veldhuizen, Todd, "Using C++ Template Metaprograms", C++ Report (May 1995) [VINOS93] Vinoski, Steve, "Distributed Object Computing with CORBA", C++ Report (July/August 1993) [VINOS94] Vinoski, Steve, "Mapping CORBA IDL into C++", C++ Report (September 1994) [YOUNG95] Young, Douglas, Object-Oriented Programming with C++ and OSF/Motif, 2d ed., Prentice-Hall(1995) 23 深度探索 C++ 对象模型(Inside The C++ Object Model ) 24 第0章 导读(译者的话) 第0章 导读 (译者的话 ) 合适的读者 很不容易㆔言两语就说明此书的适当读 者。 作者 Lippman 参与设计了全世界第 ㆒ 套 C++ 编译器 cfront ,这本书就是 ㆒位伟大的 C++ 编译器设计者向你阐述 他如 何处理各种 explicit (明白出现于 C++ 程序代码)和 implicit (隐藏于程序代码背后 ) 的 C++ 语意。 对于 C++ 程序老手,这必然是 ㆒本让你大呼过瘾的绝妙好书。 C++ 老手分两类。 ㆒种㆟把语言用得烂熟,OO 观念也有。另 ㆒种㆟不但如此, 还对于台面 ㆘的机制如编译器合成 的 default constructor 啦、object 的内存布局 啦...有莫大的兴 趣。本书对于第 ㆓类老手的吸引力自不待 言,至于第 ㆒类老手, 或许你没那么大的刨根究底的兴 趣,不过我还是非常推荐你 阅读此书。了解 C++ 对象模型,绝对有助 于你在语言本身以及对象 导向观念两方面的层次提升。 25 深度探索 C++ 对象模型(Inside The C++ Object Model ) 你需要细细 推敲每 ㆒个句子,每 ㆒个例子,囫囵吞枣是完全没有 用的。作者是 C++ 大师级㆟物,并且参与开发了第 ㆒套 C++ 编译器,他的解说以及诠释非常鞭辟 入里,你务必在看过每 ㆒小段之后,融会贯通,把思想观念化为己有 ,再接续另 ㆒小节。但阅读次序并不需要按 照书㆗的章节排 列。 阅读次序 我个㆟认为,第 1, 3, 4 章最能带给读者立即 而最大的帮助,这些都是经 常引起程 式员困惑的主题。作者在这些章节 ㆗有不少示意图(我自己也加了不少)。你或 许可以从这 ㆔章挑着看起。 其它章节比较晦涩 ㆒些(我的感觉),不妨「视可而择之」。 当然,这都是十分主观的认 定。客观 的意见只有 ㆒个:你可以随你的兴 趣与需求, 从任㆒章开始看起。各章之间没有 必然关联性。 翻译风格 太多朋友告诉我,他们阅读 ㆗文计算机书籍,不论是著 作或译作,最大的阅读困难 在于㆒大堆没有标准译名的技术名词 或习惯用语(至于那些误谬不知 所云的奇怪 作品当然本就不在考虑之列)。其实 ,就算国立编译馆有统 ㆒译名(或曾 有过, 谁知道?),流通于工业界与学 术界之间的还是原文 名词与术语。 对于工程师,我希望我所写的书和我所译的书能够让各位读来通体顺畅;对于学 生,我还希望多发挥 ㆒点引导的 力量,引导各 位多使用、多认识原文术 语和专有 名词,不要说出像「无模式对话盒( modeless dialog )」这种奇怪的话 。 由于本书读者定位之故, 我决定保留大量 的原文技术名词 与术语。 我清楚㆞知道, 在我们的技术领域里,研究 ㆟员或工程师如何使用这些语汇。 26 第0章 导读(译者的话) 当然,有些 ㆗文译名够普遍,也够 有意义,我并不排除使用。其间的 挑选与决定, 不可避免㆞带了点个㆟色彩。 ㆘面是本书出现的原文 名词(按字母排序)及其意义: 英文名词 access level access section alignment bind chain class class hierarchy composition concrete inheritance constructor data member declaration, declare definition, derived destructor encapsulation explicit hierarchy implement implementation define ㆗文名词或(及)其意义 存取层级。就是 C++ 的 public 、private 、protected ㆔种等 级。 存取区段。就是 class ㆗的 public 、private 、protected ㆔种 段落。 边界调整,调整至某些 bytes 的倍数。其结果视不同的 机器 而定。例如 32 位机器通常调整至 4 的倍数。 系结,将程序 ㆗的某个符号真 正附着 (决议) 至㆒块实体 ㆖。 串链 类别 class 体系,class 阶层架构 组合。通常与继承 (inheriance)并同讨论。 具体继承(相对于抽象继承 ) 建构式 资料成员(亦或被称为 member variable ) 宣告 定义(通常附带「在内存 ㆗挖㆒块空间」的行 为) 衍生 解构式 封装 明白的(通常指 C++ 程序代码㆗有出现的) 体系,阶层架构 实作(动词) 实作品、实作物。本书有时候指 C++ 编译器。大部份时候 27 深度探索 C++ 对象模型(Inside The C++ Object Model ) 英文名词 ㆗文名词或(及)其意义 是指 class member function 的内容。 implicit inheritance inline instance layout mangle member function members object offset operand operator overhead overload overloaded function override paradigm pointer polymorphism programming reference reference resolve 隐含的、暗喻的(通常指未出 现在 C++ 程序代码 ㆗的) 继承 行内(C++ 的㆒个关键词) 实体(有些书籍译为「案例」,极不妥 当) 布局。本书常常 出现这个字,意指 object 在内存㆗的数据 分布情况。 名称切割重组( C++ 对于函式名称的 ㆒种处理方式) 成员函式。亦或 被称为 function member 。 成员,泛指 data members 和 member functions 对象(根据 class 的宣告而完 成的㆒份占有内存的实体) 偏移位置 操作数 运算子 额外负担(因某 种设计,而导 至的额外成本) 多载 多载函式 改写(对 virtual function 的重新设计) 典范(请参考 #22 页) 指标 多型(「对象 导向」最重要的 ㆒个性质) 程序设计、程序化 参考、参用(动词)。 C++ 的 & 运算子所 代表的东西。当名词 解。 决议。函式呼叫时联结器所 做的㆒种动作,将符 号与函式实 体产生关系。如果你呼叫 func() 而联结时找 不到 func() 实 体,就会出 现 "unresolved externals" 联结错误。 表格㆗的㆒格(㆒个元素);条孔;条目;条格。 slot 28 第0章 导读(译者的话) 英文名词 subtype type virtual virtual function virtual inheritance virtual table ㆗文名词或(及)其意义 子型别 型态,型别 (指的是 int 、float 等内建型别,或 C++ classes 等自定型别) 虚拟 虚拟函式 虚拟继承 虚拟表格(为实现虚拟机制而设计的 ㆒种表格,内放 virtual functions 的地址 ) 有时候考虑到 ㆖㆘文的因素 ,面对同 ㆒个名词 ,在译与不译之间,我可能会有不 同的选择。例如,面对 "pointer" ,我会译为「指标」,但由于我并未将 reference 译为「参考」(实在不对味),所以如果原文是 "the manipulation of a pointer or reference in C++ ...",为了㆗英对等或平衡的缘故 ,我不会把它译为 「C++ ㆗对于 指标和 reference 的操作行 为...」 ,我会译为 「C++ ㆗对于 pointer 和 reference 的 操作行为...」。 译注 书㆗有㆒些译注。大部份译注,如果够短的话,会被我直接放在括号之 ㆗,接续 本文。较长的译注,则被我安 排在被注文字 的段落㆘面(紧临,并加标示)。 原书错误 这本书虽说质 ㆞极佳,制作的 严谨度却不及格!有损 Lippman 的大师 ㆞位。 属于「作者笔误」之类 的错误,比较无伤大雅,例如少了 ㆒个 ; 符号,或是多了 ㆒个 ; 符号,或是少了 ㆒个 } 符号,或是多了 ㆒个 ) 符号等等 。比较严重的错 误,是程序代码变量名称或函式名称或 class 名称与文字 叙述不㆒致,甚或是图片 ㆗对于 object 布局的画法,与程序代码 ㆗的宣告不 ㆒致。这两种错误都会严重 耗费 29 深度探索 C++ 对象模型(Inside The C++ Object Model ) 读者的心神。 只要是被我发 现的错误,都已被我修正。以 ㆘是错误更正列表。 示例:L5 表示第 5 行,L-9 表示倒 数第 9 行。页码所示为 原书页码。 页码 p.35 p.57 p.61 p.61 p.64 p.78 p.84 p.87 p.87 p.90 p.91 p.92 p.92 p.93 p.92~ p.94 p.97 p.99 码 L2 图 3.5(a) 原文位置 最后㆒行 表格第㆓行 L1 L10 L-9 最后㆕行码 图 3.1b 说明 L-2 全页多处 图 3.2a 说明 图 3.2(b) 码 L-7 码 L-6 图 3.4 说明 原文内容 Bashful(), 1.32.36 memcpy... 程序代码最后少 ㆒个 ) Shape()...程序代码最后少了 ㆒个 } 程序代码最后多了 ㆒个 ; == struct Point3d virtual... 程序代码最后少了 ㆒个 ; pc2_2 (不符合命名意义 ) Vptr placement and end of class __vptr __has_vrts class Vertex2d public Point2d Vertex2d 的物件布局 符号名称混乱,前后误谬不符 public Point3d, public Vertex 符号与书㆗程序代码多处不符 符号与书㆗程序代码多处不符 ? pv3d + ... 最后多了㆒个 ) pt1d::y & 3d_point::z; pt2d::_y &Point3d::z; pc1_2 (符合命名意义 ) Vptr placement at end of class __vptr__has_virts class Vertex3d public Point3d Vertex3d 的物件布局 已全部更改过 配合图 3.5ab ,应调整次序为 public Vertex, public Point3d 已全部更改过 已全部更改过 似乎应为 = class Point3d 应修改为 Bashful(); 1:32.36 p.100 图 3.5(b) p.100 L-2 p.106 L16 p.107 L10 30 第0章 导读(译者的话) 页码 原文位置 原文内容 & 3d_point::z; int d::*dmp, d *pd d *pd int b2::*bmp = &b2::val2; 不符合稍早出现的程序代码 magnitude() Point2d pt2d = new Point2d; Derived::~close() class Point3d... 最后少㆒个 { 应修改为 &Point3d::z; int Derived::*dmp, Derived *pd Derived *pd int Base2::*bmp = &Base2::val2; 把 pt3d 改为 Point3d magnitude3d() ptr = new Point2d; Derived::close() p.108 L6 p.108 L-6 p.109 L1 p.109 L4 p.110 L2 p.115 L1 p.126 L12 p.136 图 4.2 右㆘ p.138 L-12 p.140 程序代码 p.142 L-7 p.143 程序代码 p.145 L-6 p.147 L1 p.147 L5 p.147 L6 p.147 ㆗段码 L-1 p.148 ㆗段码 L1 p.148 ㆗段码 L-1 p.150 程序代码 p.150 L-7 p.152 L4 p.156 L7 p.160 L11, L12 p.162 L-3 p.166 ㆗,码 L3 p.166 ㆗,码 L4 没有与文字 ㆗的 class 命名㆒致 所有的 pt3d 改为 Point3d if ( this ... 程序代码最右边少 ㆒个 ) 没有与文字 ㆗的 class 命名㆒致 所有的 pt3d 改为 Point3d pointer::z() pointer::*pmf point::x() point::z() 程序代码最后缺少 ㆒个 ) (ptr->*pmf ) 函式最后少 ㆒个 ; (*ptr->vptr[.. 函式最后少 ㆒个 ) 没有与文字 ㆗的 class 命名㆒致 所有的 pt3d 改为 Point3d pA.__vptr__pt3d... 最后少㆒个 ; point new_pt; { Abstract_Base Abstract_base 函式最后少 ㆒个 ; Point1 local1 = ... Point2 local2; Point local1 = ... Point local2; Point new_pt; } Abstract_base Point::z() Point::*pmf Point::x() Point::z() 31 深度探索 C++ 对象模型(Inside The C++ Object Model ) 页码 原文位置 原文内容 Line::Line() 函式最后多了 ㆒个 ; Line::Line() 函式最后多了 ㆒个 ; Line::~Line() 函式最后多㆒个 ; Point3d::Point3d() Point3d::Point3d() y = 0.0 之前缺少 float 缺少㆒个 return const Point3d &p 缺少㆒个 return 1; new Pvertex; __nw(5*sizeof(int)); // new ( ptr_array... 程序代码少 个 ; Point2w ptw = ... operator new() 函式定义多㆒个 ; Point2w ptw = ... Point2w p2w = ... c.operator==( a + b ); x xx; x yy; struct x _1xx; struct x _1yy; struct x __0__Q1; struct x __0__Q2; if 条件句的最后多了 ㆒个 ; foo() 函式码最后多了 ㆒个 ; 应修改为 p.174 ㆗,码 L-1 p.174 ㆗㆘,码 L-1 p.175 ㆗㆖,码 L-1 p.182 ㆗㆘,码 L6 p.183 ㆖,码 L9 p.185 ㆖,码 L3 p.186 ㆗㆘,码 L6 p.187 ㆗,码 L3 p.204 ㆘,码 L3 p.208 ㆗㆘,码 L2 p.219 ㆖,码 L1 p.222 ㆖,码 L8 p.224 ㆗,码 L1 p.224 ㆘,码 L5 p.225 ㆖,码 L2 p.226 ㆘,码 L1 p.229 ㆗,码 L1 p.232 ㆗㆘,码 L2 p.232 ㆗㆘,码 L3 p.232 ㆘,码 L2 p.232 ㆘,码 L3 p.233 码 L2 p.233 码 L3 p.233 ㆗,码 p.253 码 L-1 PVertex::PVertex() PVertex::PVertex() const Point3d &p3d new PVertex; __new(5*sizeof(int)); Point2w *ptw = ... Point2w *ptw = ... Point2w *p2w = ... c.operator=( a + b ); X xx; X yy; struct X _1xx; struct X _1yy; struct X __0__Q1; struct X __0__Q2; 32 第0章 导读(译者的话) 推荐 我个㆟翻译过不少书籍,每 ㆒本都精挑细选后才动手(品质不够的原文 书,译它 做啥?!)在这么些译本当 ㆗,我从来不 做直接而露骨 ㆞推荐。好的书籍自然而然 会得到识者的欣赏。过去我译 的那些 明显具有实用价值的 书籍,总有相当数量的 读者有强烈的需求,所以我从不担 心没有足够的 ㆟来为好书散播口碑。但 Lippman 的这本书不 ㆒样,它可能不 会为你带来明显而立即的实用性,它可能因此在书肆 ㆗蒙㆖㆒层灰(其原文 书我就没 听说多少㆟读过),枉费我 从众多原文 书㆗挑出 这本好书。我担心听到这样的话: 对象模型?呵,我会写 C++ 程序,写得一级棒 ,这些属于编译器层面的东西 , 于我何有哉 ! 对象模型是深层结构的 知识,关系 到「与语言无 关、与平台无 关、跨网络可执 行」 软件组件( software component )的基础原理 。也因此 ,了解 C++ 对象模型,是 学习目前软件组件 ㆔大规格(COM、CORBA、SOM)的技术基础。 如果你对软件组件( software component )没有兴趣 ,C++ 对象模型也能够使你对 虚拟函式、虚拟继承 、虚拟接口有脱 胎换骨的新体认,或是对于各种 C++ 写法 所带来的效率利益有通盘的认 识。 我因此要大声 ㆞说:有经验的 C++ programmer 都应该看看 这本书。 如果您对 COM 有兴趣,我也要同时推荐你 看另㆒本书:Essential COM,Don Box 着,Addison Wesley 1998 出版( COM 本质论,侯捷译,碁峰 1998 )。这也是㆒ 本论述非常清楚的书籍,把 COM 的由来(为什么需 要 COM、如何使用 COM ) 以循序渐进的方式阐述 得非常深刻,是我所看过最理想的 ㆒本 COM 基础书籍。 看 Essential COM 之前,你最 好有这本 Inside The C++ Object Model 的基础。 33 深度探索 C++ 对象模型(Inside The C++ Object Model ) 34 第3章 Data 语意学( The Semantics of Data ) 第3章 Data 语意学 (The Semantics of Data ) 前些时候我 收到㆒封来自法国 的电子邮件 ,发信㆟似乎有些迷惘也有些烦乱。他 志愿(要不就是被选派)为他的项目团队提供 ㆒个「永恒的」 library。在做准备工 作的时候,他写出以 ㆘的码并打印出它们的 sizeof 结果: class class class class X Y Z A { : : : }; public virtual X { }; public virtual X { }; public Y, public Z { }; 译注:X, Y, Z, A 的继承关系如㆘图所示: X X Y Y A A Z Z 83 深度探索 C++ 对象模型(Inside The C++ Object Model ) ㆖述 X, Y, Z, A ㆗没有任何㆒个 class 内含明显 的资料,其间只表示了继承 关 系。所以发信 者认为每㆒个 class 的大小都应该是 0。当然不对 !即使是 class X 的大小也不为 0: sizeof sizeof sizeof sizeof X Y Z A 的结果为 的结果为 的结果为 的结果为 1 8 8 12 译注:以㆘是我在 Visual C++ 5.0 ㆖的执行结果 sizeof X 的结果为 1 sizeof Y 的结果为 4 sizeof Z 的结果为 4 sizeof A 的结果为 8 原因将在 p.86 的译注和 p.87 的正文 ㆗解释 让我们依序看看 每㆒个 class 的宣告,并看看它 们为什么获得㆖述结果。 ㆒个空的 class 如: // sizeof X == 1 class X { }; 事实㆖并不是空的,它有 ㆒个隐藏的 1 byte 大小,那是被编译器安插进去 的㆒个 char 。这使得此 ㆒ class 的两个 objects 得以在内存 ㆗配置独㆒无㆓的地址: X a, b; if (&a == &b) cerr << "yipes!" << endl; 令来信读者感到惊讶和沮丧的,我怀疑是 Y 和 Z 的 sizeof 结果: // sizeof Y == sizeof Z == 8 class Y : public virtual X { }; class Z : public virtual X { }; 在来信者的机器 ㆖,Y 和 Z 的大小 都是 8。这个大小和机器有关 ,也和编译器有 关。事实㆖ Y 和 Z 的大小受 到㆔个因素的影响: 1. 语言本 身所 造成 的额外 负担 (overhead ) 当 语言 支持 virtual base classes ,就会导至 ㆒些额外负担 。在 derived class ㆗,这个额外负担反 映在某种型式的指针身 ㆖,它或者指向 virtual base class subobject ,或者 指向㆒个相关表格;表格 ㆗存放的若不 是 virtual base class subobject 的 地址 ,就 是其 偏 移位 置( offset ) 。 在来 信者 的机 器 ㆖, 指标 是 4 bytes 84 第3章 Data 语意学( The Semantics of Data ) (我将在 3.4 节讨论 virtual base class )。 2. 编译器对于特殊情况所提供的最佳化处理 Virtual base class X subobject 的 1 bytes 大小也出现在 class Y 和 Z 身㆖。传统 ㆖它被放在 derived class 的固定(不变动)部份的尾端。某些编译器会对 empty virtual base class 提 供 特 殊 支 援 ( 以 ㆘ 第 ㆔ 点 之 后 的 ㆒ 段 文 字 对 此 有 比 较 详 细 的 讨 论)。来信读者所使用的编译器,显然并 未提供这项特 殊处理。 3. Alignment 的限制 class Y 和 Z 的大小截至目前为 5 bytes 。在大 部份机器㆖, 群聚的结构 体大小会受到 alignment 的限制 ,使它们能 够 更 有 效 率 ㆞ 在 记 忆 体 ㆗ 被 存 取 。 在 来 信 读 者 的 机 器 ㆖ , alignment 是 4 bytes , 所 以 class Y 和 Z 必 须 填 补 3 bytes 。 最 终 得 到 的 结 果 就 是 8 bytes 。 译注:alignment 就是将数值调整到某数 的整数倍。在 32 位计算机 ㆖,通常 alignment 为 4 bytes (32 位), 以使 bus 的「运输量」达到最高效 率。 译注:我以 ㆘图表现㆖述的 X, Y, Z 对象布局 bytes 4 1 3 bytes 4 1 3 derived class Y char onebyte; (because Y is empty class) alignment padding derived class Z virtual base class X subobject char onebyte; char onebyte; (because X is empty class) (because X is empty class) char onebyte; char onebyte; (because Z is empty class) (because Z is empty class) alignment padding alignment padding 注意:不同的 编译器对于物 件的 members 可能有不同 的排列次序。 85 深度探索 C++ 对象模型(Inside The C++ Object Model ) Empty virtual base class 已经成为 C++ OO 设计的㆒个特有术语了。它提供 ㆒个 virtual interface ,没有定义任何数据 。某些新近的编译器 (译注)对此提供了特殊 处理(请看 [SUN94a] )。在这个策略之 ㆘,㆒个 empty virtual base class 被视为 derived class object 最开头的㆒部份,也就是说它并没有花费 任何的额外空间。这 就节省了㆖述第2点的 1 bytes (译注:因为既 然有了 members ,就不需要原本 为了 empty class 而安插的㆒个 char ),也就不再需要第 3 点所说的 3 bytes 的填 补。只剩㆘第1点所说的 额外负担。在此模型 ㆘,Y 和 Z 的大小 都是 4 而不是 8。 译注:Visual C++ 就是㆖述这㆒类型的编译器。我以 ㆘图来表现 Visual C++ 对 于 class X, Y, Z 的物件布局: derived class Y (4 bytes) Visual C++ 对于 X, Y, Z 的对象 布局 virtual base class X subobject char onebyte; char onebyte; (because X is empty class) (because X is empty class) derived class Z (4 bytes) 编译器之间的潜 在差异正说明了 C++ 对象模型的演化。这个模型为 ㆒般情况提 供了解决之道 。当特殊情况 逐渐被挖掘出来,种种 启发(尝试 错误)法于是被引 入,提供最佳化的处 理。如果成功,启发法于是就提升为普遍的策略,并跨越各 种实作品而合并。它被 视为标准(虽然它并不被规范为标准),久而久之也就成 了语言的㆒部份。 Virtual function table 就是㆒个好例子,另 ㆒个例子是第2章讨 论过的「named return value (NRV)最佳化」。 86 第3章 Data 语意学( The Semantics of Data ) 那么,你期望 class A 的大小是什么呢?很明显 ,某种程度 ㆖必须视你所 使用的 编译器而定。首先,请你考虑那种并未特别处理 empty virtual base class 的编译 器。如果我们忘记 Y 和 Z 都是「虚拟衍生」自 class X,我们可能会回答 16 , 毕竟 Y 和 Z 的大小 都是 8。然而当我们对 class A 施以 sizeof 运算子,得到的 答案竟然是 12 。到底怎么回事 ? 记住,㆒个 virtual base class subobject 只会在 derived class ㆗存在㆒份实体,不 管它在 class 继承体系㆗出现了多少次 !class A 的大小由㆘列数点决定: 被大家共享的唯 ㆒㆒个 class X 实体,大小为 1 byte 。 Base class Y 的大小,减去「因 virtual base class X 而配置」的大小 ,结 果是 4 bytes 。Base class Z 的算法亦同。加起来是 8 bytes 。 class A 自己的大小 :0 byte 。 class A 的 alignment 数量(如果有的话)。前述 ㆔项总合,表示调整前 的大小是 9 bytes 。class A 必须调整至 4 bytes 边界,所以需要填补 3 bytes 。结果是 12 bytes 。 现在如果我们考虑那种「特别对 empty virtual base class 做了处理」的编译器呢? ㆒如前述, class X 实体的那 1 byte 将被拿掉,于是额外的 3 bytes 填补额 也不必 了,因此 class A 的大小将是 8 bytes 。注意,如果我们在 virtual base class X ㆗ 放置㆒个(以㆖)的 data members ,两种编译器 (「有特殊处理 」者和 「没有特 殊处理」者)就会产生 出完全相 同的对象布局。 C++ Standard 并不强制规定如 「base class subobjects 的排列次序」或「不同存取 层级的 data members 的排列次序」 这种琐碎细 节。 它也不规定 virtual functions 或 virtual base classes 的实作细节。 C++ Standard 只说:那些 细节由各家厂商自定 。 我在本章以及全书 ㆗,都会区分「C++ Standard 」和「目前的 C++ 实作标准」两 种讨论。 87 深度探索 C++ 对象模型(Inside The C++ Object Model ) 在这㆒章㆗,class 的 data members 以及 class hierarchy 是㆗心议题。 ㆒个 class 的 data members ,㆒般而言,可以表现这个 class 在程序执行时的某种状态。 Nonstatic data members 放置的是「个别的 class object 」感兴趣的数据 ,static data members 则放置的是「整个 class 」感兴趣的资料 。 C++ 对象模型尽量以空间最佳化和存取速度最佳化的考虑来表现 nonstatic data members ,并且保持和 C 语言 struct 数据配置的兼 容性。它把数据直接存放在每 ㆒个 class object 之㆗。对于继承 而来的 nonstatic data members 不管是 virtual 或 ( 也是如此。不过并没有 强制定义其间的 排列顺序。至于 static nonvirtual base class ) data members 则被放置 在程序的 ㆒个 global data segment ㆗,不会影响个别的 class object 的大小。在程序之 ㆗,不管该 class 被产生 出多少个 objects (经由直 接产生或间接衍生),static data members 永远只存在 ㆒份实体(译注:甚至即 使 该 class 没有任何 object 实体,其 static data members 也已存在)。但是㆒个 template class 的 static data members 的行为稍有不同, 7.1 节有详细的讨论。 每 ㆒ 个 class object 因 此 必 须 有 足 够 的 大 小 以 容 纳 它 所 有 的 nonstatic data members 。有时候其值可能令你吃惊 (正如那位法国来信者 ),因为它可能比你想 像的还大,原因是: 1. 由编译器自动加 ㆖的额外 data members ,用以支持某些语言特性 (主要 是各种 virtual 特性)。 2. 因为 alignment (边界调整 )的需要。 3.1 Data Member 的系结(The Binding of a Data Member ) 考虑㆘面这段程序代码: // 某个 foo.h 表头档,从某处含入 extern float x; 88 第3章 Data 语意学( The Semantics of Data ) // 程序员的 Point3d.h 档案 class Point3d { publi c: Point3d( float, float, float ); // 问题:被传回和被设定的 x 是哪㆒个 x 呢? float X() const { return x; } void X( float new_x ) const { x = new_x; } // ... private: float x, y, z; }; 如 果 我 问 你 Point3d::X() 传 回 哪 ㆒ 个 x ? 是 class 内 部 那 个 x , 还 是 外 部 (extern)那个 x?今㆝每个㆟都会回答我是内部那 ㆒个。这个答案是正确的 ,但 并不是从过去以来 ㆒直都正确! 在 C++ 最早的编译器 ㆖,如果在 Point3d::X() 的两个函式实体 ㆗对 x 做出参阅 (取用)动作,这动作将会指向 global x object !这样的系结结果几乎普遍 ㆞不在 大家的预期之㆗,并因此导 出早期 C++ 的两种防御性程序设计风格: 1. 把所有的 data members 放在 class 宣告起头处 ,以确保正确的系结 : class Point3d { // 防御性程序设计风格 #1 // 在 class 宣告起头处 先放置 所有 data member float x, y, z; public: float X() const { return x; } // ... etc. ... }; 2. 把所有的 inline functions ,不管大小都放在 class 宣告之外: class Point3d { public: // 防御性程序设计风格 #2 // 把所有的 inlines 都移到 class 之外 Point3d(); 89 深度探索 C++ 对象模型(Inside The C++ Object Model ) float X() const; void X( float ) const; // ... etc. ... }; inline float Point3d: : X() const { return x; } // ... etc. ... 这些程序设计风格事实 ㆖到今㆝还存在,虽然它们的 必要性已经自从 C++ 2.0 之 后(伴随着 C++ Reference Manual 的修订)就消失了。这个古早的语言规则被 称为 "member rewriting rule" ,大意是 「㆒个 inline 函式实体,在整个 class 宣告 未被完全看见之前,是不会被评估求值 (evaluated) 。C++ Standard 以 "member 的」 scope resolution rules" 来精炼这个 "rewriting rule" ,其效果是,如果㆒个 inline 函 式在 class 宣告之后立刻被 定义的话,那么就还是对其评估求值( evaluate)。也 就是说,当 ㆒个㆟写㆘这样的码: extern int x; class Point3d { public: // 对于函式本体的分析将延 迟直至 // class 宣告的右大括号出现才开始。 float X() const { return x; } // ... private: float x; }; // 事实㆖,分析在 这里进行 对 member functions 本体的分析,会直到整个 class 的宣告都出现了才开始。因 此在㆒个 inline member function 躯体之内的㆒个 data member 系结动作,会在整 90 第3章 Data 语意学( The Semantics of Data ) 个 class 宣告完成之后才发生。 然而,这对 于 member function 的 argument list 并不为真。 Argument list ㆗的名 称还是会在它们第 ㆒次遭遇时被适当 ㆞决议( resolved)完成。因此在 extern 和 nested type names 之间的 非直觉系结 动作还是会发生。例如在 ㆘面的程序片 段 ㆗,length 的型别在两个 member function signatures ㆗都决议 (resolve) global 为 ,也就是 int 。当后续再 有 length 的 nested typedef 宣告出 现,C++ Standard typedef 就把稍早的系结 标示为非法: typedef int length; class Point3d { public: // 喔欧: length 被决议(resolved)为 global // 没问题: _val 被决议(resolved)为 Point3d::_val void mumble( length val ) { _val = val; } length mumble() { return _val; } // ... private: // length 必须在「本 class 对它的第 ㆒个参考动作」之前被看 见。 // 这样的宣告将使先前的参考动作不合法。 typedef float length; length _val; // ... }; ㆖述这个语言状况,仍然需要某种防御性程序风格:请总是把「 nested type 宣告」 放在 class 的起始处。在 ㆖述例子 ㆗,如果把 length 的 nested typedef 定义于 「在 class ㆗被参考」之前,就可以确保非直觉系结 的正确性。 91 深度探索 C++ 对象模型(Inside The C++ Object Model ) 3.2 Data Member 的布局(Data Member Layout ) 已知㆘面㆒组 data members : class Point3d { public: // ... private: float x; static List *freeList; float y; static const int chunkSize = 250; float z; }; , Nonstatic data members 在 class object ㆗的排列顺序将和其被宣 告的顺序 ㆒样 任 何㆗间介入的 static data members 如 freeList 和 chunkSize 都不会被放进对象布 局之㆗。在㆖述例子里,每 ㆒个 Point3d 对象是由㆔个 float 组成,次序是 x, y, z。static data members 存放在程序的 data segment ㆗,和个别的 class objects 无 关。 C++ Standard 要求,在同 ㆒个 access section (也就是 private 、public 、protected 等 区段)㆗,members 的排列只需符合「较晚出 现的 members 在 class object ㆗有 较高的地址 」 这㆒条件即可 (请看 C++ Standard 9.2 节) 。也就是说各 个 members 并不㆒定得连续排列。什么 东西可能会介于被宣 告的 members 之间呢? members 的边界调整( alignment)可能就需要填补 ㆒些 bytes 。对于 C 和 C++ 而言这的 确是真的,对目前的 C++ 编译器实作情况 而言,这也是 真的。 编译器还可能会合成 ㆒些内部使用的 data members 以支持整个对象模 型 vptr 就 , 。 是这样的东 西,目前所有的编译器都把它安插在每 ㆒个「内含 virtual function 之 class 」的 object 内。vptr 会被放在什么 位置呢?传统 ㆖它被放在所有明白宣告的 members 的最后头。不过如今 也有㆒些编译器把 vptr 放在㆒个 class object 的最 92 第3章 Data 语意学( The Semantics of Data ) 前端。C++ Standard 秉持先前所说的 「对于布局所 持的放任态度」,允许编译器 把那些内部产生出来的 members 自由放在任何位 置㆖ 甚至放在那些被程 序员宣 , 告出来的 members 之间。 C++ Standard 也允许编译器将多个 access sections 之㆗的 data members 自由排 列,不必在乎它们出现在 class 宣告㆗的次序。也就是说, ㆘面这样的宣告: class Point3d { public: // ... private: float x; static List *freeList; private: float y; static const int chunkSize = 250; private: float z; }; 其 class object 的大小和组成都和我们先前宣告的那个相同,但是 members 的排 列次序则视编 译器而定。编译器可以随意 把 y 或 z 或什么东西放第 ㆒个,不过 就我所知道,目前没有 任何编译器会这么做。 目前各家编 译器都是把 ㆒个以㆖的 access sections 连锁在 ㆒起,依照宣告的次 序,成为㆒个连续区块。 Access sections 的多寡并不会招来额 外负担。例如在 ㆒ 个 section ㆗宣告 8 个 members ,或是在 8 个 sections ㆗总共宣告 8 个 members , 得到的 object 大小是㆒样的。 ㆘面这个 template function ,接受两个 data members ,然后判断谁先出现在 class object 之㆗。如果两 个 members 都是不同的 access sections ㆗的第㆒个被宣告 者,此函式就可以用来判断哪 ㆒个 section 先出现(如果你对 class member 的指 标并不熟悉,请参考 3.6 节): 93 深度探索 C++ 对象模型(Inside The C++ Object Model ) template< class class_type, class data_type1, class data_type2 > char* access_order( data_type1 class_type: :*mem1, data_type2 class_type: :*mem2 ) { assert (mem1 != mem2 ); return mem1 < mem2 ? "member 1 occurs first" : "member 2 occurs first"; } ㆖述函式可 以这样被唤起: access_order( &Point3d: :z, &Point3d: :y); 于是 class_type 会被系结 为 Point3d ,而 data_type1 和 data_type2 会被系结为 float 。 3.3 Data Member 的存取 已知㆘面这段程序代码: Point3d origin; origin.x = 0.0; 你可能会问 x 的存取成本是什么 ?答案视 x 和 Point3d 如何宣告而定。 x 可能 是个 static member ,也可能是个 nonstatic member 。Point3d 可能是个独立(非衍 生)的 class ,也可能是从另 ㆒个单㆒的 base class 衍生而来;虽然可能性不高, 但它甚至可能是多重继承 或虚拟继承 而来。㆘面数节将依次检验每 ㆒种可能性。 在开始之前,让我 先丢出 ㆒个问题。如果我们有两个定 义,origin 和 pt : Point3d origin, *pt = &origin; 94 第3章 Data 语意学( The Semantics of Data ) 我用它们来存取 data members ,像这样: origin.x = 0.0; pt ->x = 0.0; 透过 origin 存取,和透过 pt 存取,有什么重大差异吗?如果你的回答是 yes , 请你从 class Point3d 和 data member x 的角度来说明差 异的发生因素 。我会在这 ㆒节结束前重 返这个问题并提出我的答 案。 Static Data Members Static data members ,按其字 面意义 ,被编译器提出于 class 之外,㆒如我在 1.1 节 所说,并被视为 ㆒个 global 变量(但只在 class 生命范围之内可见)。每 ㆒个 member 的存取许可(译注: private 或 protected 或 public ),以及与 class 的关 联,并不会招致任何空间 ㆖或执行时间 ㆖的额外负担 -- 不论是在个别的 class objects 或是在 static data member 本身。 每㆒个 static data member 只有㆒个实体,存放在程序的 data segment 之㆗。每次 程序参阅(取用) static member ,就会被内部转化为对该唯 ㆒之 extern 实体的直 接参考动作。例如: // origin.chunkSize == 250; Point3d::chunkSize == 250; // pt->chunkSize == 250; Point3d::chunkSize == 250; // 译注:我想作者的意思可能是要说 // Point3d::chunkSize = 250; // 译注:我想作者的意思可能是要说 // Point3d: :chunkSize = 250; 从指令执行的 观点来看,这是 C++ 语言㆗「透过 ㆒个指针和透过 ㆒个对象来存 取 member ,结论完全相同 」的唯 ㆒㆒种情况 。这是因为 「经由 member selection operators (译注:也就是 '.' 运算子)对 ㆒个 static data member 做存取动作」只 是文法㆖的㆒种便宜行事而已。 member 其实并不在 class object 之㆗,因此存取 static members 并不需要透过 class object 。 95 深度探索 C++ 对象模型(Inside The C++ Object Model ) 但如果 chunkSize 是㆒个从复杂继承 关系㆗继承而来的 member ,又当如何 ?或 许它是㆒个 「virtual base class 的 virtual base class 」 (或其它同等复杂的继承 架构) 的 member 也说不定。哦,那无关紧要,程序之 ㆗对于 static members 还是只有 唯㆒㆒个实体,而其存取路径仍然是那么直接。 如果 static data member 的存取是经 由函式呼叫,或其 它某些语法 呢?举个例子, 如果我们写: foobar().chunkSize == 250; // 译注:我想作者的意思可能是要说 // foobar().chunkSize = 250; 唤起 foobar() 会发生什么 事情?在 C++ 的准标准( pre-Standard)规格㆗,没有 ㆟ 知 道 会 发 生 什 么 事 , 因 为 ARM 并 未 指 定 foobar() 是 否 必 须 被 求 值 (evaluated)。cfront 的作法是简单 ㆞把它丢掉。但 C++ Standard 明白要求 foobar() 必须被求值 (evaluated) ,虽然其结果并无用 处。㆘面是㆒种可能的转 化: // foobar().chunkSize == 250; // 译注:我想作者的意思是要说 // foobar().chunkSize = 250; // evaluate expression, discarding result (void) foobar(); Point3d. chunkSize == 250; // 译注:我想作者的意思是要说 // Point3d. chunkSize = 250; 若取㆒个 static data member 的地址,会得到 ㆒个指向其数据型别 的指针,而不是 ㆒个指向其 class member 的指标,因为 static member 并不内含在㆒个 class object 之㆗。例如: &Point3d: :chunkSiz e; 会获得型态如㆘的内存地址 : const int* 96 第3章 Data 语意学( The Semantics of Data ) 如果有两个 classes ,每㆒个都宣告了 ㆒个 static member freeList ,那么当它们都被 放在程序的 data segment ,就会导至名称冲突 。编译器的解决方法是暗 ㆗对每㆒ 个 static data member 编码(这种手法有个很美的名称: name-mangling),以获得 ㆒个独㆒无㆓的程序识别代 码。有多少个编译器,就有 多少种 name-mangling 作 法!通常不外乎 是表格啦、文法措辞啦 等等。任何 name-mangling 作法都有两个 重点: 1. ㆒个算法,推导出独 ㆒无㆓的名称。 2. 万㆒编译 系统( 或环境 工具)必 须和使 用者交 谈,那些 独㆒无 ㆓的名 称 可以轻易被 推导回到原来的名称。 Nonstatic Data Members Nonstatic data members 直接存放在每 ㆒个 class object 之㆗。除非经由明白的 (explicit)或暗喻的( implicit)class object ,没有办法直接存取它们 。只要程序员 在㆒个 member function ㆗直接处理 ㆒个 nonstatic data member ,所谓 "implicit class object" 就会发生。例如 ㆘面这段码: Point3d Point3d::translate( const Point3d &pt) { x += pt.x; y += pt.y; z += pt.z; } 表面㆖所看到的对于 x, y, z 的直接存取,事实 ㆖是经由㆒个 "implicit class object" (由 this 指标表达)完成。事实 ㆖这个函式的参数是: // member function 的内部转化 Point3d Point3d::translate( Point3d *const this, const Point3d &pt) { this->x += pt.x; this->y += pt.y; this->z += pt.z; } 97 深度探索 C++ 对象模型(Inside The C++ Object Model ) Member functions 在本书第4章有比较详细 的讨论。 欲对㆒个 nonstatic data member 做存取动作,编译器需 要把 class object 的起始位 址加㆖ data member 的偏移位置( offset)。举个例子,如果: origin._y = 0.0; 那么地址 &origin._y 将等于: &origin + (&Point3d::_y - 1); 请注意其㆗的 -1 动作。指向 data member 的指标,其 offset 值总是被加 ㆖ 1, 这样可以使编译系统区分出「 ㆒个指向 data member 的指标 ,用以指出 class 的 第㆒个 member 」和「㆒个指向 data member 的指标,没有指出任何 member 」两 种情况。「指向 data members 的指标」将在 3.6 节有比较详细 的讨论。 每㆒个 nonstatic data member 的偏移位置( offset)在编译时期即可获 知,甚至如 果 member 属于㆒个 base class subobject (衍生自单 ㆒或多重继承串链 )也是㆒ 样。因此,存取 ㆒个 nonstatic data member ,其效率和存取 ㆒个 C struct member 或 ㆒个 nonderived class 的 member 是㆒样的。 现在让我们看看虚拟继承 。虚拟继承 将为「经由 base class subobject 存取 class members 」导入㆒层新的间接性 ,譬如: Point3d *pt3d; pt3d->_x = 0.0 其执行效率在 _x 是㆒个 struct member 、㆒个 class member 、单㆒继承、多重继 承的情况㆘都完全相 同。但如果 _x 是㆒个 virtual base class 的 member ,存取速 度会比较慢 ㆒点。㆘㆒节我会验证「继承 对于 member 布局的影响」。在我们尚 未进行到那里之前,请回忆 本节㆒开始的㆒个问题:以两种方法存取 x 坐标,像 这样: 98 第3章 Data 语意学( The Semantics of Data ) origin.x = 0.0; pt->x = 0.0; 「从 origin 存取」和「从 pt 存取」有什么重大的差异?答案是「当 Point3d 是 ㆒个 derived class ,而其继承架构 ㆗有㆒个 virtual base class ,并且被存取的 member (如本例的 x)是㆒个从该 virtual base class 继承而来的 member 」时,就 会有重大的差异。这时候我 们不能够说 pt 必然指向哪 ㆒种 class type (因此我们 也就不知道编译时期这个 member 真正的 offset 位置),所以这个存取动作必须 延迟至执行时期,经由 ㆒个额外的间 接导引,才能 够解决。但如果使用 origin , 就不会有这 些问题,其型态无疑是 Point3d class ,而即使它继承自 virtual base class ,members 的 offset 位置也在编译时期就固定 了。㆒个积极进取的编译器甚 至可以静态 ㆞经由 origin 就解决 掉对 x 的存取。 3.4 「继承 」与 Data Member 在 C++ 继承模型㆗,㆒个 derived class object 所表现 出来的东 西,是其自己的 members 加㆖其 base class(es) members 的总合。至于 derived class members 和 base class(es) members 的排列次序并未在 C++ Standard ㆗强制指定;理论 ㆖编译 器可以自由安 排之。在大部份编译器 ㆖头,base class members 总是先出现,但属 于 virtual base class 的除外(㆒般而言,任何 ㆒条通则㆒旦碰㆖ virtual base class 就没辄儿,这里亦不例外)。 了解这种继承 模型之后,你可能会问,如果我为 2D (㆓维)或 3D (㆔维)坐标 点提供两个抽象数据型态如 ㆘: // supporting abstract data types class Point2d { public: // constructor(s) // operations // access functions private: float x, y; }; 99 深度探索 C++ 对象模型(Inside The C++ Object Model ) Point2d Point2d Point3d Point3d class Point3d { public: // constructor(s) // operations // access functions private: float x, y, z; }; 这和「提供两层或 ㆔层继承架构,每 ㆒层(代表 ㆒个维度)是 ㆒个 class ,衍生自 较低维层次」有什么 不同?㆘面各小节的讨论将涵盖「单 ㆒继承且不含 virtual functions 」、「单㆒继承并含 virtual functions 」、「多重继承」、「虚拟继承」 等㆕种情况 。图 3.1a 就是 Point2d 和 Point3d 的对象布局图,在没有 virtual functions 的情况 ㆘(如本例),它们和 C struct 完全㆒样。 float x; float y; struct Point2d { float x, y; } pt2d; float x; float y; float z; struct Point3d { float x, y, z; } pt3d; 图 3.1a 个 别 structs 的 资 料 布 局 只要继承不要多型 (Inheritance without Polymorphism ) 想象㆒㆘,程序员或许希望,不论是 2D 或 3D 坐标点,都能 够共享同 ㆒个实体, 但又能够继 续使用「与型别 性质相关(所谓 type-specific )」的实体。我们有㆒个 设计策略,就是从 Point2d 衍生出㆒个 Point3d ,于是 Point3d 将继承 x 和 y 座 标的㆒切(包括数据实 体和操作方法)。带来的影响则是可 以共享「数据本身」 以及 「数据的处 理方法」 并将之区域化 ㆒般而言 具体继承 , 。 , (concrete inheritance , 译注:相对于虚拟继承 virtual inheritance )并不会增加空间或存取时间 ㆖的额外 负担。 100 第3章 Data 语意学( The Semantics of Data ) class Point2d { public: Point2d( float x = 0.0, float y = 0.0 ) : _x( x ), _y( y ) { }; float x() { return _x; } float y() { return _y; } void x( float newX ) { _x = newX; } void y( float newY ) { _y = newY; } void operator+=( const Point2d& rhs ) { _x += rhs. x(); _y += rhs. y(); } // ... more members protected: float _x, _y; }; Point2d Point2d Point3d Point3d // inheritance from concrete class class Point3d : public Point2d { public : Point3d( float x = 0.0, float y = 0.0, float z = 0.0 ) : Point2d( x, y ), _z( z ) { }; float z() { return _z; } void z( float newZ ) { _z = newZ; } void operator+=( const Point3d& rhs ) { Point2d::operator+=( rhs ); _z += rhs.z(); } // ... more members prot ected: float _z; }; 这样子设计的好处就是可 以把管理 x 和 y 坐标的程序代码区域化。此外这个设计 可以明显表现出两个抽象类别之 间的紧密关系 。当这两个 classes 独立的时候, 101 深度探索 C++ 对象模型(Inside The C++ Object Model ) Point2d object 和 Point3d object 的宣告和使用都不 会有所改变。所以这两个抽象 类别的使用者不 需要知道 objects 是否为独立的 classes 型态,或是彼此 之间有继 承的关系。图 3.1b 显示 Point2d 和 Point3d 继承关系的实物布局,其间并没有 宣告 virtual 界面。 float _x; float _y; class Point2d { public: ... protected: float _x, _y; } pt2d; float _x; float _y; float _z; Point2d subobject class Point3d : public Point2d { public: ... protected: float _z; } pt3d; 图 3.1b 单 一 继 承 而 且 没 有 virtual function 时 的 资 料 布 局 把两个原本独 立不相干的 classes 凑成㆒对 "type/subtype" ,并带有继承关系 ,会 有什么易犯的错误呢?经验不足的 ㆟可能会重复 设计㆒些相同动作的 函式。以我 们例子㆗的 constructor 和 operator+= 为例,它们并没有 被做成 inline 函式(也 可能是编译器为了某些理由没有 支持 inline member functions ) 。Point3d object 的 初始化动作或加法动作,将需要部份的 Point2d object 和部份的 Point3d object 做为成本。 ㆒般而言,选择 某些函式做成 inline 函式,是设计 class 时的㆒个重 要课题。 第㆓个易犯的错误是 ,把㆒个 class 分解为两层或更多层 ,有可能会为 了「表现 class 体系之抽象化」而膨胀所需 空间。C++ 语言保证「出现在 derived class ㆗ 的 base class subobject 有其完整 原样性」 ,正是重点所在 。这似乎有点 难以理解! 最好的解释方法就是彻底了解 ㆒个实例,让我 们从㆒个具体的 class 开始: 102 第3章 Data 语意学( The Semantics of Data ) 4 1 1 1 1 int val char c1 char c2 char c3 padding Concrete object class Concrete { public: // ... private: int val; char c1; char c2; char c3; }; 在㆒部 32 位机器 ㆗,每㆒个 Concrete class object 的大小都是 8 bytes ,细分 如㆘: 1. val 占用 4 bytes 2. c1 和 c2 和 c3 各占用 1 bytes 3. alignment (调整到 word 边界)需要 1 bytes 现在假设 经过某些分析之后 我们决定了 ㆒个更逻辑的表达方式 把 Concrete 分 , , , 裂为㆔层架构: class Concrete1 { public: // ... private: int val; char bit1; }; class Concrete2 : public Concrete1 { public: // ... private: char bit2; }; class Concrete3 : public Concrete2 { public: // ... private: char bit3; }; Concrete1 Concrete1 Concrete2 Concrete2 Concrete3 Concrete3 103 深度探索 C++ 对象模型(Inside The C++ Object Model ) 从设计的观点来看,这个架构可能比较合理。但从实 务的观点来看,我们可能会 受困于㆒个事实:现在 Concrete3 object 的大小是 16 bytes ,比原先的设计多了 ㆒ 倍。 怎么回事,还记 得「base class subobject 在 derived class ㆗的原样性 」吗?让我 们 踏遍这㆒继承架构的内存布局,看看 到底发生了什么 事。 Concrete1 内含两个 members :val 和 bit1 ,加起来是 5 bytes 。而㆒个 Concrete1 object 实际用掉 8 bytes ,包括填补用的 3 bytes ,以使 object 能够符合 ㆒部机器 的 word 边界。不论是 C 或 C++ 都是这样。 ㆒般而言,边界调整( alignment) 是由处理器( processor)来决定。 到目前为止 没什么需 要抱怨。但这种典型的布局会导至轻 率的程序员犯 ㆘错误。 Concrete2 加了唯 ㆒㆒个 nonstatic data member bit2 ,数据型态为 char 。轻率的程 式员以为它会和 Concreate1 包捆在㆒起,占用 原本用来填补空间的 1 bytes ;于 是 Concreate2 object 的大小为 8 bytes ,其㆗ 2 bytes 用于填补空间。 然而 Concrete2 的 bit2 实际㆖却是被放在填补空间所用 的 3 bytes 之后。于是其 大小变成 12 bytes ,不是 8 bytes 。其㆗有 6 bytes 浪费在填补空间 ㆖。相同道理 使得 Concrete3 object 的大小是 16 bytes ,其㆗ 9 bytes 用于填补空间。 『真是愚蠢』,我们那位纯真小 甜甜这么说。许多读者以电 子邮件 、电话、或是 嘴巴对我也这么说。你可了解为什么 这个语言有这 样的行为? 104 第3章 Data 语意学( The Semantics of Data ) 译注:㆘图可说明 Concreate1 、Concreate2 、Concreate3 的物件布局: 4 1 3 int val char bit1 padding 3 bytes Concrete1 object 1 3 char bit2 padding 3 bytes Concrete2 object 1 3 char bit3 padding 3 bytes Concrete3 object 8 Concrete1 subobject 12 Concrete2 subobject 让我们宣告以 ㆘㆒组指标 : Concrete2 *pc2; Concrete1 *pc1_1, *pc1_2; 其㆗ pc1_1 和 pc1_2 两者都 可以指向前述 ㆔种 classes objects 。㆘面这个指定动 作: *pc1_2 = *pc1_1; 应该执行㆒个预设的 "memberwise" 复制动作(复制 ㆒个个的 members ),对象 是被指之 object 的 Concrete1 那㆒部份。如果 pc1_1 实际指向 ㆒个 Concrete2 object 或 Concrete3 object , 则 ㆖述 动 作 应该 将 复 制内 容 指 定给 其 Concrete1 subobject 。 然 而 , 如 果 C++ 语 言 把 derived class members ( 也 就 是 Concrete2::bit2 或 105 深度探索 C++ 对象模型(Inside The C++ Object Model ) Concrete3::bit3 )和 Concrete1 subobject 包捆在㆒起,去除填补空间, ㆖述那些 语 意就无法保 留了,那么 ㆘面的指定动作: pc1_1 = pc2; // 译注:令 pc1_1 指向 Concrete2 物件 // 喔欧: derived class subobject 被覆写掉 , // 于是其 bit2 member 现在有了㆒个并非预期的数值 *pc1_2 = *pc1_1; 就会将「被包捆在 ㆒起、继承而得的」 members 内容覆写掉 。程序员必须花费极 大的心力才能找出这个臭虫! 译注:让我 以图形解释。如果「 base class subobject 在 derived class ㆗的原样性 」 受到破坏 也就是说 编译器把 base class object 原本的 填补空间让出来给 derived , , class members 使用,像这样: 4 1 3 int val char bit1 padding 3 bytes Concrete1 object 4 1 1 int val 4 1 1 1 1 int val char bit1 char bit2 char bit3 padding Concrete3 object char bit1 char bit2 padding 2 2 bytes Concrete2 object 那么当发生 Concrete1 subobject 的复制动作时,就会破坏 Concrete2 members 。 4 1 3 int val char bit1 padding 3 bytes Concrete1 object copy 4 1 1 int val bit2 被指定了 ㆒ 个数值,此非吾 ㆟所期望 char bit1 char bit2 padding 2 2 bytes Concrete2 object 106 第3章 Data 语意学( The Semantics of Data ) 加上多型 (Adding Polymorphism ) 如果我要处理㆒个坐标点,而不打算在乎它是 ㆒个 Point2d 或 Point3d 实体,那 么我需要在继承 关系㆗提供㆒个 virtual function 接口。让我 们看看如果这么做, 情况会有什么 改变: // 译注:以 ㆘的 Point2d 宣告请与 #101 页的宣告做 比较 class Point2d { public: Point2d( float x = 0.0, float y = 0.0 ) : _x( x ), _y( y ) { }; // x 和 y 的存取函式与前 ㆒版相同。 // 由于对不同维 度的点,这些函式动作固定 不变,所以不必 设计为 virtual // 加㆖ z 的保留空间(目前什么 也没做) virtual float z() { return 0.0; } virtual void z( float ) { } // 设定以㆘的运算子为 virtual virtual void operator+=( const Point2d& rhs ) { _x += rhs. x(); _y += rhs .y(); } // ... more members protected: float _x, _y; }; // 译注:2d 点的 z 为 0.0 是合理的 只有当我们企图以多型的方式( polymorphically)处理 2d 或 3d 坐标点,在设计 之㆗导入㆒个 virtual 界面才显合理。也就是说,写 ㆘这样的码: void foo( Point2d &p1, Point2d &p2 ) { // ... p1 += p2; // ... } 其㆗ p1 和 p2 可能是 2d 也可能是 3d 坐标点。这并不是先前任何设计所能支 107 深度探索 C++ 对象模型(Inside The C++ Object Model ) 援的。这样的弹性,当然正是对象 导向程序设计的 ㆗心。支持这 样的弹性,势必 对我们的 Point2d class 带来空间和 存取时间的额外负担: 导 入 ㆒ 个 和 Point2d 有 关 的 virtual table , 用 来 存 放 它 所 宣 告 的 每 ㆒ 个 virtual functions 的 位 址 。 这 个 table 的 元 素 个 数 ㆒ 般 而 言 是 被 宣 告 之 virtual functions 的 个数 ,再加 ㆖㆒ 个或两 个 slots (用 以支 援 runtime type identification )。 在每㆒个 class object ㆗导入㆒个 vptr ,提供执行时期的联结 ,使㆒个 object 能够找到相 应的 virtual table 。 加 强 constructor , 使 它 能 够 为 vptr 设 定 初 值 , 让 它 指 向 class 所 对 应 的 virtual table 。 这 可 能 意 味 在 derived class 和 每 ㆒ 个 base class 的 constructor ㆗ , 重 新 设 定 vptr 的 值 。 其 情 况 视 编 译 器 的 最 佳 化 的 积 极 性而定。第5章对此有比较详细 的讨论。 加 强 destructor , 使 它 能 够 抹 消 「 指 向 class 之 相 关 virtual table 」 的 vptr 。 要 知 道 , vptr 很 可 能 已 经 在 derived class destructor ㆗ 被 设 定 为 derived class 的 virtual table 地址。记住, destructor 的呼叫次序是 反向 的:从 derived class 到 base class 。㆒个积极的最佳化编译器可以压抑 大量的那些 指定动作。 这些额外负担带来的冲击程 度视「被处理的 Point2d objects 的个数和生命期」而 定,也视「对这 些 objects 做多型程序设计所得的利益」而定。如果 ㆒个应用程 式知道它所能使用的 point objects 只限于㆓维坐标点或 ㆔维坐标点,这种设计所 带来的额外负担可能变得令 ㆟无法接 受1。 以㆘是新的 Point3d 宣告: // 译注:以 ㆘的 Point3d 宣告请与 #101 页的宣告做 比较 class Point3d : public Point2d { public: 1 我不知道是否有哪个 产品系统真 正使用了㆒个多型的 Point 类别体系。 108 第3章 Data 语意学( The Semantics of Data ) Point3d( float x = 0.0, float y = 0.0, float z = 0.0 ) : Point2d( x, y ), _z( z ) { }; float z() { ret urn _z; } void z( float newZ ) { _z = newZ; } void operator+=( const Point2d& rhs ) // 译注:注意 ㆖行是 Point2d& 而非 Point3d& Point2d::operator+=( rhs ); _z += rhs.z(); } // ... more members protected: float _z; }; 译注:㆖述新的(与 p.101 比较)Point2d 和 Point3d 宣告,最大 ㆒个好处是, 你可以把 operator+= 运用在㆒个 Point3d 对象和㆒个 Point2d 物件身 ㆖: Point2d p2d(2.1, 2.2); Point3d p3d(3.1, 3.2, 3.3); p3d += p2d; 得到的 p3d 新值将是 (5.2, 5.4, 3.3); 虽然 class 的宣告语法 没有改变,但每㆒件事情都不 ㆒样了:两个 z() member functions 以及 operator+=() 运算子都成了虚拟函式;每 ㆒个 Point3d class object 内含㆒个额外的 vptr member 继承自 Point2d ) ( ;多了㆒个 Point3d virtual table ; 此外每㆒个 virtual member function 的唤起也比以前复杂了 (第4章对此有详细 说 明)。 目前在 C++ 编译器那个领域里有 ㆒个主要的讨论题目:把 vptr 放置在 class object 的哪里会最好?在 cfront 编译器㆗,它被放在 class object 的尾端,用以 支持㆘面的继承类型,如图 3.2a 所示: struct no_virts { int d1, d2; }; 109 深度探索 C++ 对象模型(Inside The C++ Object Model ) class has_virts : public no_virts { public: virtual void foo(); // ... private: int d3; }; no_virts *p = new has_virts; int d1 int d2 struct no_virts nv; int d1 int d2 int d3 __vptr__has_virts class has_virts : public no_virts hv; no_virts subobject 图 3.2a Vptr 被 放 在 class 的 尾 端 把 vptr 放在 class object 的尾端,可以保 留 base class C struct 的对象布局,因而 允许在 C 程序代码 ㆗也能使用。这种作法在 C++ 最初问世时 ,被许多㆟采用。 到了 C++ 2.0 ,开始支持虚拟继承以及抽象基础类别 ,并且由于对象导向典范 (OO paradigm )的出头 ,某些编译器开始把 vptr 放到 class object 的起头处 (例如 Martin O'Riordan ,他领导 Microsoft 的第㆒个 C++ 编译器产品,就十 分主张这 种作法)。请看图 3.2b 的图解说明。 110 第3章 Data 语意学( The Semantics of Data ) int d1 int d2 struct no_virts nv; __vptr__has_virts int d1 int d2 int d3 class has_virts : public no_virts hv; no_virts subobject 图 3.2b Vptr 被 放 在 class 的 前 端 把 vptr 放在 class object 的前端,对于 「在多重继承 之㆘, 透过指向 class members 的指标,唤起 virtual function 」,会带来㆒些帮助(请参考 4.4 节)。否则,不 仅 「从 class object 起始点开始量起 」 offset 必须在执行时期备 妥,甚至与 class 的 vptr 之间的 offset 也必须备妥。当然, vptr 放在前端,代价就是丧 失了 C 语言 兼容性。这种丧失有 多少意义?有多少程序会从 ㆒个 C struct 衍生出 ㆒个具多型 性质的 class 呢?目前我手 ㆖并没有什么统计数据可以告诉我这 ㆒点。 图 3.3 显示 Point2d 和 Point3d 加㆖了 virtual function 之后的继承 布局。注意 此图是把 vptr 放在 base class 的尾端。 float _x float _y __vptr__Point2d class Point2d p2d; float _x float _y __vptr__Point2d float _z class Point3d : public Point2d pt3d; Point2d subobject 图 3.3 单一继承并含虚拟函式之情况下的数据布局 111 深度探索 C++ 对象模型(Inside The C++ Object Model ) 多重继承 (Multiple Inheritance ) 单㆒继承提供了㆒种 「自然多型 (natural polymorphism ) 形式,是关于 classes 体 」 系㆗的 base type 和 derived type 之间的转换 请看图 3.1b 图 3.2a 或图 3.3 , 。 、 你会看到 base class 和 derived class 的 objects 都是从相同的 地址开始,其间差 异只在于 derived object 比较大,用以多容纳它 自己的 nonstatic data members 。㆘ 面这样的指定动作: Point3d p3d; Point2d *p = &p3d; 把㆒个 derived class object 指定给 base class (不管继承深度有多深 )的指标或 reference 。这动作并不需要编译 器去调停或修改地址 。它很自然 ㆞可以发生,而 且提供了最佳执行效率。 图 3.2b 把 vptr 放在 class object 的起始处 如果 base class 没有 virtual function 。 而 derived class 有(译注:正如图 3-2b ),那么单 ㆒继承的自然多型 (natural 就会被打破。这种情况 ㆘,把㆒个 derived object 转换为 其 base 型 polymorphism ) 态,就需要编译器的介入,用以调整地址 (因 vptr 插入之故)。在既是多重继承 又是虚拟继承 的情况㆘,编译器的介入更有必要。 多重继承既不像单 ㆒继承,也不容易模塑 出其模型。多重继承 的复杂度在于 derived class 和其㆖㆒个 base class 乃至于㆖㆖㆒个 base class...之间的「非自 然」关系。例如,考虑 ㆘面这个多 重继承所获得的 class Vertex3d : 译注:原书的 p92~p94 有很多前后不 ㆒致的㆞方,以及很多「本身虽没有错 误却 可能误导读者思 想」的叙述。程序代码和图片说明也不相符,简直 ㆒团乱!我已将 之全部更正。如果您拿着原文 书对照此 ㆗译本看,请不要乍见之 ㆘对我产生误会。 class Point2d { public: 112 第3章 Data 语意学( The Semantics of Data ) // ... (译注:拥有 virtual 界面。所以 Point2d 对象之㆗会有 vptr ) protected: float _x, _y; }; class Point3d : public Point2d { public: // ... protected: float _z; }; class Vertex { public: // ... (译注:拥有 virtual 界面。所以 Vertex 对象之㆗会有 vptr ) protected: Vertex *next; }; class Vertex3d : // 译注:原书误把 Vertex3d 写为 Vertex2d public Point3d, public Vertex { // 译注:原书误把 Point3d 写为 Point2d public: // ... protected: float mumble; }; 译注:至此, Point2d、Point3d、Vertex、Vertex3d 的继承关系如㆘: Point2d Point2d Point3d Point3d Vertex Vertex Vertex3d Vertex3d 多重继承的问题主要发 生于 derived class objects 和其第 ㆓或后继的 base class objects 之间的转 换;不论是直接转换如 ㆘: 113 深度探索 C++ 对象模型(Inside The C++ Object Model ) extern void mumble( const Vertex& ); Vertex3d v; ... // 将㆒个 Vertex3d 转换为 ㆒个 Vertex 。这是「不自然的 」。 mumble( v ); 或是经由其所支持的 virtual function 机制做转换。因支援「 virtual function 之唤 起动作」而引发的问题将在 4.2 节讨论。 对㆒个多重衍生对象,将其地址指定给「最左端(也就是第 ㆒个)base class 的指 标」,情况 将和单 ㆒继承时相同,因为 ㆓者都指向相同的 起始地址 。需付出的成 本只有地址 的指定动作而 已(图 3.4 显示出多重继承的布局)。至于第 ㆓个或后 继的 base class 的地址指定动作,则需要将地址 修改过:加 ㆖(或减去,如果 downcast 的话)介于㆗间的 base class subobject(s) 大小,例如: Vertex3d v3d; Vertex *pv; Point2d *p2d; Point3d *p3d; // 译注:原书命名 为 *pp ,不符合命名原则 。改为 *p2d 较佳。 // 译注:Point3d 的定义请看图 3.3 和 #108 页 那么㆘面这个指定动作: pv = &v3d; 需要这样的内部转化: // 虚拟 C++ 码 pv = (Vertex*)(((char*)&v3d) + sizeof( Point3d )); 而㆘面的指定动作: p2d = &v3d; p3d = &v3d; 都只需要简单 ㆞拷贝其地址 就好。如果有两个指标 如㆘: Vertex3d *pv3d; Vertex *pv; // 译注:原书命名 为 *p3d ,不符命名通则 。改为 *pv3d 较佳。 114 第3章 Data 语意学( The Semantics of Data ) 那么㆘面的指定动作: pv = pv3d; 不能够只是简单 ㆞被转换为 : // 虚拟 C++ 码 pv = (Vertex*)((char*)pv3d) + sizeof( Point3d ); 因为如果 pv3d 为 0,pv 将获得 sizeof( Point3d ) 的值。这是错误的!所以,对 于指标,内部转换动作需要有 ㆒个条件测试: // 虚拟 C++ 码 pv = pv3d ? (Vertex*)((char*)pv3d) + sizeof( Point3d ) : 0; 至于 reference ,则不需要针对可能的 0 值做防卫,因为 reference 不可能参考到 「无物」( no object )。 float _x float _y __vptr__Point2d class Point2d pt2d; float _x float _y __vptr__Point2d float _z class Point3d : public Point2d pt3d; 图 3.4 float _x Point2d subobject Vertex *next __vptr__Vertex Vertex v; Point2d subobject float _y __vptr_ _Point2d float _z Vertex *next __vptr__Vertex float mumble class Vertex3d : public Point3d, public Vertex { } v3d; Vertex subobject Point3d subobject 资 料 布 局 : 多 重 继 承 ( Multiple Inheritance ) 115 深度探索 C++ 对象模型(Inside The C++ Object Model ) 译注:原书的图 3.4 只画出 Vertex2d ,没有画出 Vertex3d 。虽然其 ㆗的 Vertex2d 物 件布局图「可能」是正确的(我们并没有 在书㆗看到其宣告码),但我相信这 其 实是 Lippman 的笔误,因为它与书 ㆗的许多讨论没有 关系。所以我把真正与书 ㆗ 讨论有关的 Vertex3d 的对象布局画于译本的 图 3.4 ,如㆖。 C++ Standard 并未要求 Vertex3d ㆗的 base classes Point3d 和 Vertex 有特定的 排列次序。原始的 cfront 编译器是把它们根据宣告次序来排列。因此 cfront 编 译器制作出来的 Vertex3d 对象,将可被视为是 ㆒个 Point3d subobject (其㆗又有 ㆒个 Point2d subobject )加㆖㆒个 Vertex subobject ,最后再加 ㆖ Vertex3d 自己的 部份。目前各编译器仍然是以此方式完成多重 base classes 的布局(但如果加 ㆖ 虚拟继承,就不㆒样)。 某些编译器(例如 MetaWare )设计有 ㆒种最佳化技术 ,只要第㆓个(或后继) base class 宣告了㆒个 virtual function ,而第㆒个 base class 没有,就把多个 base classes 的次序调换。这样可 以在 derived class object ㆗少产生㆒个 vptr 。这项最 佳化技术并未得到全 球各厂商的认 可,因此并不普及。 如果要存取第 ㆓个(或后 继)base class ㆗的㆒个 data member ,将会是怎样的情 况?需要付出额外的成本吗?不, members 的位置在编译时就固定 了,因此存取 members 只是㆒个简单的 offset 运算,就像单 ㆒继承㆒样简单 -- 不管是经由㆒ 个指标或是㆒个 reference 或是㆒个 object 来存取。 虚拟继承 (Virtual Inheritance ) 多重继承的㆒个语意㆖的副作用就是,它必须支持某种型式的「 shared subobject 继承」。典型的 ㆒个例子是最早的 iostream library : 116 第3章 Data 语意学( The Semantics of Data ) // pre-standard iostream implementation class ios { ... }; class istream : public ios { ... }; class ostream : public ios { ... }; class iostream : public istream, public ostream { ... }; 译注:㆘图可表现 iostream 的继承 体系图。左为多 重继承 ,右为虚 拟多重继承 。 ios ios istream istream iostream iostream ios ios ostream ostream istream istream iostream iostream ios ios ostream ostream 不论是 istream 或 ostream 都内含㆒个 ios subobject 。然而在 iostream 的对象 布 局㆗,我们只需要单 ㆒㆒份 ios subobject 就好。语言层面 的解决办法是导入所谓 的虚拟继承 : class ios { ... }; class istream : public virtual ios { ... }; class ostream : public virtual ios { ... }; class iostream : public istream, public ostream { ... }; ㆒如其语意所呈现 的复杂度,要在编译器 ㆗支持虚拟继承 ,实在是困难度 颇高。 在㆖述 iostream 例子㆗,实作技术的挑战在 于,要找到 ㆒个足够有效的方法,将 istream 和 ostream 各自维护的 ㆒个 ios subobject ,折迭成为 ㆒个由 iostream 维 护的单㆒ ios subobject ,并且还可以保存 base class 和 derived class 的指标(以 及 references )之间的多型指定动作 (polymorphism assignments )。 ㆒般的实作法如㆘所述。Class 如果内含㆒个或多个 virtual base class subobjects , 像 istream 那样,将被分割为 两部份:㆒个不变区域和 ㆒个共享区域。不变区域 117 深度探索 C++ 对象模型(Inside The C++ Object Model ) ㆗的数据,不管后继如何衍 化,总是拥有固定 的 offset 从 object 的起头算起) ( , 所以这㆒部份数据可以被直接存取。至于共享区域,所表现 的就是 virtual base class subobject 。这㆒部份的数据 ,其位置会因为每次衍生动作而有变化 ,所以它 们只可以被间接存取。各家编 译器实作技术之间的 差异就在于间接存取的方法不 同。以㆘说明㆔种主流策略。 ㆘面是 Vertex3d 虚拟继承 的阶层架构 2: class Point2d { public: ... protected: float _x, _y; }; class Vertex : public virtual Point2d { public: ... protected: Vertex *next; }; class Point3d : public virtual Point2d { public: ... protected: float _z; }; class Vertex3d : public Vertex, public Point3d // 译注:原书 ㆖㆒行的两 个 classes 次序相反。为与图 3.5ab 配合,故改之。 { public: ... protected: float mumble; }; 2 这个阶层架构是 [POKOR94] 所倡议的,那是 ㆒本很好的 3D Graphics 教科书,使用 C++ 语言。 118 第3章 Data 语意学( The Semantics of Data ) 译注:㆘图可表现 Point2d 、Point3d 、Vertex 、Vertex3d 的继承体系: Point2d Point2d Vertex Vertex _x, _y next Point3d _z Point3d Vertex3d mumble Vertex3d ㆒般的布局策略是先安排好 derived class 的不变部份,然后 再建立其共享部份。 然而,这㆗间存在着 ㆒个问题:如何能 够存取 class 的共享部份呢? cfront 编译 器会在每㆒个 derived class object ㆗安插㆒些指标,每个指标指 向㆒个 virtual base class 。要存取继承得来的 virtual base class members ,可以藉由相关指标间接 完成。举个例,如果我们有以 ㆘的 Point3d 运算子: void Point3d:: operator+=( const Point3d &rhs ) { _x += rhs. _x; _y += rhs._y; _z += rhs._z; }; 在 cfront 策略之㆘,这个运算子会被内部转换为 : // 虚拟 C++ 码 __vbcPoint2d->_x += rhs.__vbcPoint2d->_x; // 译注:vbc 意为: __vbcPoint2d ->_y += rhs. __vbcPoint2d ->_y; // virtual base class _z += rhs. _z; 而㆒个 derived class 和㆒个 base class 的实体之 间的转换,像这样: Point2d *p2d = pv3d; // 译注:原书为 Vertex *pv = pv3d; 恐为笔误 119 深度探索 C++ 对象模型(Inside The C++ Object Model ) 在 cfront 实作模型之 ㆘,会变成 : // 虚拟 C++ 码 Point2d *p2d = pv3d ? pv3d ->__vbcPoint2d : 0; // 译注:原书为 Vertex *pv = pv3d ? pv3d->__vbcPoint2d : 0; 恐为笔误(感谢黄俊达先生与刘东岳先生来信指导) // 这样的实作模型有两个主要的缺点: 1. 每㆒个对象必须针对其每 ㆒个 virtual base class 背负㆒个额外的指标 。 然 而 理 想 ㆖ 我 们 却 希 望 class object 有 固 定 的 负 担 , 不 因 为 其 virtual base classes 的个数而有所变 化。想想看 这该如何解决 ? 2. 由于虚拟 继承串 链的加 长,导至 间接存 取层次 的增加。 这意思 是,如 果 我有㆔层虚拟衍化,我就 需要㆔次间接存取(经由 ㆔个 virtual base class 指标)。 然而理 想㆖我 们却希望 有固定 的存取 时间,不 因为虚 拟衍化 的 深度而改变。 MetaWare 和其它编译器到 今㆝仍然使用 cfront 的原始实作模型来解决 第㆓个问 题,它们经由拷贝动作取得所有的 nested virtual base class 指标,放到 derived class object 之㆗。这就解决了「固定 存取时间」的问题,虽然付出了 ㆒些空间㆖ 的代价。MetaWare 提供㆒个编译时期的选项 ,允许程序员选择 是否要产生 双重 指标。图 3.5a 说明这种「以指标指 向 base class 」的实作模型 。 至于第㆒个问题, ㆒般而言有两个解决 方法。 Microsoft 编译器引入 所谓的 virtual base class table 。每㆒个 class object 如果有 ㆒个或多个 virtual base classes ,就会 由编译器安插 ㆒个指标,指向 virtual base class table 。至于真正的 virtual base class 指针,当然是被放在该表格 ㆗。虽然此法已行之有年,但我并不知 道是否有其它 任何编译器使用此法。说不定 Microsoft 对此法提出专利,以至别 ㆟不能使用它。 第㆓个解决 方法,同时也是 Bjarne 比较喜欢的方法(至少当我还和他共事于 ,是在 virtual function table ㆗放置 virtual base class 的 offset Foundation 专案时) (而不是地址 )。图 3.5b 显示这种 base class offset 实作模型。我在 Foundation 120 第3章 Data 语意学( The Semantics of Data ) float _x float _y __vptr__Point2d Point2d pt2d; Vertex* next Point2d *pPoint2d __vptr__Vertex float _z float _z Point2d *pPoint2d __vptr__Point3d float _x float _y __vptr__Point2d class Point3d : virtual Point2d { ... } pt3d; Point2d subobject Vertex* next Point2d *pPoint2d __vptr__Vertex float _x float _y __vptr__Point2d class Vertex : virtual Point2d { ... } v; Point2d subobject Point2d *pPoint2d __vptr__Point3d float mumble float _x float _y __vptr__Point2d class Vertex3d : public Vertex, public Point3d { ... } v3d; Point2d subobject Point3d subobject Vertex subobject 图 3.5a 虚 拟 继 承 , 使 用 Pointer Strategy 所 产 生 的 资 料 布 局 ( 译 注 : 原 书 图 3.5a 把 Vertex 写 为 Vertex2d , 与 书 中 程 式 码 不 符 , 所 以 我 全 部 改 为 Vertex ) 专案㆗实作出此法,将 virtual base class offset 和 virtual function entries 混杂在㆒ 起。在新近的 Sun 编译器 ㆗,virtual function table 可经由正值或负值来索引。如 果是正值,很显然 就是索引到 virtual functions ;如果是负值 ,则是索引到 virtual base class offsets 。在这样的策略之 ㆘,Point3d 的 operator+= 运算子必须被 转换 为以㆘形式(为了可读性,我没有 做型别转换,同时我也没有 先执行对效率有帮 助的地址预先计算动作): // 虚拟 C++ 码 (this + __vptr__Point3d[-1])->_x += (&rhs + rhs.__vptr__Point3d[-1])->_x; (this + __vptr__Point3d[-1])->_y += (&rhs + rhs.__vptr__Point3d[-1])->_y; _z += rhs._z; 121 深度探索 C++ 对象模型(Inside The C++ Object Model ) 虽然在此策略之 ㆘,对于继承 而来的 members 做存取动作,成本会比较昂贵,不 过此成本已经被分散至 「对 member 的使用」㆖,属于区域性成本。Derived class 实体和 base class 实体之 间的转 换动作,例如: Point2d *p2d = pv3d; // 译注:原书为 Vertex *pv = pv3d; 恐为笔误 在㆖述实作模型 ㆘将变成 : // 虚拟 C++ 码 Point2d *p2d = pv3d ? pv3d + pv3d->__vptr__Point3d[-1] : 0; // 译注:㆖㆒行原书为: // Vertex *pv = pv3d ? pv3d + pv3d ->__vptr__Point3d[ -1] : 0; 恐为笔误(感谢黄俊达先生与刘东岳先生来信指导) // ㆖述每㆒种方法都 是㆒种实作模型,而不是 ㆒种标准。每 ㆒种模型都是用来解决 「存取 shared subobject 内的数据(其位置会因每次衍生动作而 有变化)」所引 发的问题。由于对 virtual base class 的支持带来额 外的负担以 及高度的复杂性, 每㆒种实作模型多少有点 不同,而且 我想还会随着时间而 进化。 经由㆒个非 多型的 class object 来存 取㆒个继 承而来的 virtual base class 的 member ,像这样: Point3d origin; ... origin._x; 可以被最佳化为 ㆒个直接存取动作,就好像 ㆒个经由对象 唤起的 virtual function 呼叫动作,可以在编译时期被决 议(resolved)完成 ㆒样。在这次 存取以及㆘㆒次 存取之间,对象 的型别不可以改变,所以「 virtual base class subobjects 的位置会 变化」的问题在 此情况㆘就不再 存在了。 ㆒般而言,virtual base class 最有效的㆒种运用形式就是: ㆒个抽象的 virtual base class ,没有任何 data members 。 122 第3章 Data 语意学( The Semantics of Data ) float _z 8 __vptr__Point3d float _x Point2d subobject float _y __vptr__Point2d class Point3d : virtual Point2d { ... } pt3d; virtual base class offsets (8) virtual function slots beginning of virtual table ... Vertex* next 8 __vptr__Vert ex float _x Point2d subobject float _y __vptr__Point2d class Vertex : virtual Point2d { ... } v; ... ... ... (8) Vertex subobject Vertex* next __vptr__Vertex float _z __vptr__Point3d float mumble float _x 12 20 ... ... ... ... (12) (20) Point3d subobject Point2d subobject float _y __vptr__Point2d class Vertex3d : public Vertex, public Point3d { ... } v3d; ... 图 3.5b 虚 拟 继 承,使 用 Virtual Table Offset Strategy 所 产 生 的 资 料 布 局 ( 译 注 : 原 书 图 3.5b 把 Vertex 写 为 Vertex2d , 与 书 中 程 式 码 不 符 , 所 以 我 全 部 改 为 Vertex ) 123 深度探索 C++ 对象模型(Inside The C++ Object Model ) 3.5 对象成员的效率 (Object Member Efficiency ) ㆘面数个测试,旨在 量测聚合( aggregation)、封装( encapsulation)、以及继承 (inheritance)所引发的额外负荷 的程度。所有量测都是以个别区域变量的加法、 减法、指派( assign)等动作的 存取成本为依据: ㆘面就是个别的区域变量: float pA_x = 1.725, float pB_x = 0.315, pA_y = 0.875, pB_y = 0.317, pA_z = 0.478; pB_z = 0.838; 每个表达式需执行 ㆒千万次,如 ㆘所示 (当然啦,㆒旦坐标点的表现 方式有变化, 运算语法也就得随 之变化): for ( int { pB_x = pB_y = pB_z = } iters = 0; iters < 10000000; iters++ ) pA_x - pB_z; pA_y + pB_x; pA_z + pB_y; 我们首先针 对㆔个 float 元素所 组成的区域数组进行测试: enum fussy { x, y, z }; for ( int iters = 0; { pB[ x ] = pA[ x ] pB[ y ] = pA[ y ] pB[ z ] = pA[ z ] } iters < 10000000; iters++ ) - pB[ z ]; + pB[ x ]; + pB[ y ]; 第㆓个测试是把同质的数组元素转换为 ㆒个 C struct 数据抽象型别 ,其㆗的成员 皆为 float ,成员名称是 x, y, z: for ( int { pB.x = pB.y = pB.z = } iters = 0; iters < 10000000; iters++ ) pA.x - pB.z; pA.y + pB.x; pA.z + pB.y; 124 第3章 Data 语意学( The Semantics of Data ) 更深㆒层的抽象化,是做出 数据封装,并使用 inline 函式。坐标点现在 以㆒个独 立的 Point3d class 来表示。我尝试 两种不同型式的存取函式,第 ㆒,我定义㆒个 inline 函式,传回 ㆒个 reference ,允许它出现在 assignment 运算子的两 端: class Point3d { public: Point3d( float xx = 0.0, float yy = 0.0, float zz = 0.0 ) : _x( xx ), _y( yy ), _z( zz ) { } float& x() float& y() float& z() { return _x; } { return _y; } { return _z; } private: float _x, _y, _z; }; 那么真正对每 ㆒个坐标元素的存取动作应 该是像这样: for ( int { pB. x() pB. y() pB. z() } iters = 0; iters < 10000000; iters++ ) = pA. x() - pB. z(); = pA. y() + pB. x(); = pA. z() + pB. y(); 我所定义的第 ㆓种存取函式型式是 ,提供㆒对 get/set 函式: float x() { return _x; } void x( float newX ) { _x = newX; } // 译注:此即 get 函式 // 译注:此即 set 函式 于是对每㆒个坐标值的 存取动作应 该像这样: pB.x( pA.x() - pB.z() ); 表格 3.1 列出两种编译器对于 ㆖述各种测试的结果。只有当两个编译器的效率有 明显差异时,我才会把 两者分别列出。 125 深度探索 C++ 对象模型(Inside The C++ Object Model ) 表 格 3.1 不断加强抽象化程度之后 ,数据的存取效率 最佳化 0.80 未最佳化 1.42 个别的区域变量 区域数组 CC NCC struct 之㆗有 public 成员 0.80 0.80 0.80 2.55 1.42 1.42 class 之㆗有 inline Get 函式 CC 0.80 NCC 0.80 class 之㆗有 inline Get & Set 函式 CC 0.80 NCC 0.80 2.56 3.10 1.74 2.87 这里所显示的重点在于,如果把最佳化开关打 开,「 封装」就不会带来执行时期 的效率成本。使用 inline 存取函式亦然 。 我很奇怪为 什么在 CC 之㆘存取数组,几乎比 NCC 慢两倍,尤其是数组存取所 牵扯的只是 C 数组,并没有 用到任何复杂的 C++ 特性。㆒位程序代码产生 (code 专家将这种反常现象解释为 「㆒种奇行怪癖 ...与特定的编译器有关」 。 generation ) 或许是真的,但它发生在我正用来开发软件的编译器身 ㆖耶!我决定挖掘其 ㆗秘 密。叫我 「爱挑毛病的乔治」 吧,如果你喜欢的话!如果你对此 ㆒题目不感兴趣, 请直接跳往 ㆘㆒个主题。 在㆘面的 assembly 语言输出片段㆗,l.s 表示载入 (load)㆒个单精度浮 点数, s.s. 表示储存 (store)㆒个单精度浮 点数,sub.s 表示将两个单精度浮 点数相减。 ㆘面是两种编译器的 assembly 语言输出结果,它们都加载两个值,将某 ㆒个减 去另㆒个,然后储存其结果。在效 率较差的 CC 编译器 ㆗,每㆒个区域变量的位 址都被计算并放 进㆒个缓存器之㆗(addu 表示无正负号 的加法): 126 第3章 Data 语意学( The Semantics of Data ) // CC assembler output # 13 pB[ x ] = pA[ x ] - pB[ z ]; addu $25, $sp, 20 l. s $f4, 0($25) addu $24, $sp, 8 l. s $f6, 8($24) sub. s $f8, $f4, $f6 s.s $f8, 0($24) 而在 NCC 编译器的 assembly 输出㆗,加载( load)步骤直 接计算地址 : // NCC assembler output # 13 pB[ x ] = pA[ x ] - pB[ z ]; l.s $f4, 20($sp) l. s $f6, 16($sp) sub. s $f8, $f4, $f6 s. s $f8, 8($sp) 如果区域变量被存取多次, CC 策略或许比较有效率。然而对于单 ㆒存取动作, 把变量地址 放到㆒个缓存器 ㆗很明显 ㆞增加了表达式的成本。不论哪 ㆒种编译 器,只要把最佳化开关打 开,两段码都会变得相同,在其 ㆗,回路内的所有运算 都会以缓存器 内的数值来执行。 让我㆘㆒个结论 如果没有把最佳化开关打 开, : 就很难猜测 ㆒个程序的效率表现 , 因为程序代码潜在性 ㆞受到专家所谓的「 ㆒种奇行怪癖 ...与特定编译器有关」的魔 咒影响。在你开 始「原始码层面 的最佳化动作」以加速程序的运作之前,你应该 先确实㆞量测效率,而不是靠着推 论与常识判断。 在㆘㆒个测试㆗,我首先要介绍 Point 抽象化的㆒个㆔层单㆒继承表达法,然后 再介绍 Point 抽象化的 ㆒个虚拟继承 表达法。我要 测试直接存取和 inline 存取 (多重继承 并不适用于此 ㆒模型,所以我决定放弃它 )。㆔层单㆒继承表达法如 ㆘: class Point1d { ... }; class Point2d : public Point1d { ... }; class Point3d : public Point2d { ... }; // 维护 x // 维护 y // 维护 z 127 深度探索 C++ 对象模型(Inside The C++ Object Model ) 「单层虚拟继承」是从 Point1d ㆗虚拟衍生出 Point2d ;「双层虚拟继承 」则又 从 Point2d ㆗虚拟衍生出 Point3d 。表格 3.2 列出两种编译器的测试结果。同样 ㆞,只有当两种编译器的效率有明显 不同时,我才会把 两者分别列出。 表 格 3.2 在继承模型之下的数据存取 最佳化 未最佳化 1.42 2.55 3.10 单㆒继承 直接存取 使用 inline 函式 CC NCC 虚拟继承(单层) 直接存取 使用 inline 函式 CC NCC 虚拟继承(双层) 直接存取 CC NCC 使用 inline 函式 CC NCC 0.80 0.80 0.80 1.60 1.60 1.60 1.94 2.75 3.30 2.25 3.04 2.25 2.50 2.74 3.68 3.22 3.81 单㆒继承应该不会影响测试的效率,因为 members 被连续储存 于 derived class object ㆗,并且其 offset 在编译时期就已知了。测试结果 ㆒如预期,和表格 3.1 ㆗的抽象数据型别结 果相同。这结果在多重继承 的情况㆘应该也是 相同的,但我 不能确定。 再㆒次,值得注意的是,如果把最佳化关闭,以常识来判断,我们说 效率应该相 同(对于「直接存取」和「 inline 存取」两种作法)。然而实际 ㆖却是 inline 存 取比较慢。我们再次得到教训:程序员如果关心 其程序效率,应该实际量测,不 要光凭推论或常识判断或假设。另 ㆒个需要注意的是,最佳化动作并不 ㆒定总是 128 第3章 Data 语意学( The Semantics of Data ) 能够有效运 作,我不只 ㆒次以最佳化方式来编译 ㆒个已通过编译的正常程序,却 以失败收场。 虚拟继承的效率令 ㆟失望!两种编译器都没能够辨识出对「继承 而来的 data member pt1d::_x 」的存取系透过 ㆒个非多型对象 (因而不需要执行时期的间接存 取)。两个编译器都会对 pt1d::_x (及双层虚拟继承 ㆗的 pt2d::_y )产生间接存 取动作,虽然其 在 Point3d 对象㆗的位置早在编译时期就固定 了。「间接性」压 抑了「把所有运算都移往缓存器 执行」的最佳化能力。但是间接性并不会严重 影 响非最佳化程序的执行效率。 3.6 指向 Data Members 的指标 (Pointer to Data Members ) 指向 data members 的指标 ,是㆒个有点神秘但颇有用处的 语言特性,特别是如果 你需要详细 调查 class members 的底层布局的话。这样的调查可用以决定 vptr 是 放在 class 的起始处或是尾端。另 ㆒个用途,展现于 3.2 节,可用来决定 class ㆗ 的 access sections 的次序。㆒如我曾说过,那是 ㆒个神秘但有时候有 用的语言特 性。 考虑㆘面的 Point3d 宣告。其 ㆗有㆒个 virtual function ,㆒个 static data member , 以及㆔个坐标值: class Point3d { public: virtual ~Point3d(); // ... protected: static Point3d origin; float x, y, z; }; 每㆒个 Point3d class object 含有㆔个坐标值,依序为 x, y, z,以及㆒个 vptr 。至 于 static data member origin 将被放在 class object 之外。唯 ㆒可能因编译器不同 129 深度探索 C++ 对象模型(Inside The C++ Object Model ) 而不同的是 vptr 的位置。 C++ Standard 允许 vptr 被放在对象 ㆗的任何位置:在 起始处,在尾端,或是在各个 members 之间。然而实际 ㆖,所有编译器不是把 vptr 放在对象的头,就是放在对象 的尾。 那么,取某个坐标成员 的地址,代表什么 意思?例如,以 ㆘动作所得到的值 代表 什么: & Point3d::z; // 译注:原书的 & 3d_point::z; 应为笔误 ㆖述动作将得到 z 坐标在 class object ㆗的偏移位置( offset)。最低限度其 值将 是 x 和 y 的大小 总和,因为 C++ 语言要求同 ㆒个 access level ㆗的 members 的排列次序应该和其宣告次序相同。 然而 vptr 的位置就没有 限制。不过容我 再说㆒次,实际㆖ vptr 不是放在对象 的 头,就是放在对象 的尾。在 ㆒部 32 位机器㆖,每㆒个 float 是 4 bytes ,所以 我们应该期望刚才获得 的值要不是 8 就是 12 在 32 位机器㆖㆒个 vptr 是 4 , ( bytes )。 然而,这样的期望却还少 1 bytes 。对于 C 和 C++ 程序员而言,这多少算是 个 有点年代的错误了。 如果 vptr 放在对象 的尾巴,㆔个坐标值在对象 布局㆗的 offset 分别是 0, 4, 8。 如果 vptr 放在对象 的起头,㆔个坐标值在对象 布局㆗的 offset 分别是 4, 8, 12 。 然而你若去取 data members 的地址,传回的值 总是多 1,也就是 1, 5, 9 或 5, 9, 13 等等。你知道为什么 Bjarne 决定要这么做吗? 译注:如何取 & Point3d::z 的值并打印出来?以 ㆘是示范作法: printf("&Point3d::x = %p\n", &Point3d::x); printf("&Point3d::y = %p\n", &Point3d::y); printf("&Point3d::z = %p\n", &Point3d::z); // 结果 VC5 :4,BCB3 :5 // 结果 VC5 :8,BCB3 :9 // 结果 VC5 :C,BCB3 :D 130 第3章 Data 语意学( The Semantics of Data ) 注意,不可以这么做: cout << "&Point3d: :x = " << &Point3d: :x << endl; cout << "&Point3d: :y = " << &Point3d: :y << endl; cout << "&Point3d::z = " << &Point3d::z << endl; 否则会得到错误讯息 : error C2679: binary '<<' : no operator defined which takes a right-hand operand of type 'float Point3d::*' (or there is no acceptable conversion) (new behavior; please see help) 我使用的编译器是 Microsoft Visual C++ 5.0 。为什么执行结果并不如书 ㆗所说增 加 1 呢?原因可能是 Visual C++ 做了特殊处理,其道理与 本章㆒开始对于 empty virtual base class 的讨论相近! 问题在于,如何区分 ㆒个「没有 指向任何 data member 」的指标 ,和㆒个指向「第 ㆒个 data member 」的指标?考虑这样的例子 : float Point3d::*p1 = 0; float Poin t3d: :*p2 = &Point3d: :x; // 译注:Point3d::* 的意思是:「指向 Point3d data member 」之指标型别 。 // 喔欧:如何区分? if ( p1 == p2 ) { cout << " p1 & p2 contain the same value -- "; cout << " they must address the same member!" << endl; } 为了区分 p1 和 p2 ,每㆒个真正的 member offset 值都被加 ㆖ 1。因此,不论编译 器或使用者都必 须记住,在真正使用该值以指出 ㆒个 member 之前,请先减掉 1。 认识「指向 data members 的指标 」之后,我们发现,要解释: & Point3d::z; // 译注:原书的 & 3d_point::z; 应为笔误 和 & origin.z 131 深度探索 C++ 对象模型(Inside The C++ Object Model ) 之间的差异,就非常明确了 。鉴于「取 ㆒个 nonstatic data member 的地址,将会 得到它在 class ㆗的 offset 」,取㆒个「系结于真正 class object 身㆖的 data member 」的地址,将会得到该 member 在内存 ㆗的真正地址 。把 & origin.z 所得结果减(译注:原文 为加,错误) z 的偏移值(相对于 origin 起始地址), 并加 1(译注:原文为减 ,错误 ),就会得到 origin 起始地址 。㆖㆒行的传回值 型别应该是: float* 而不是 float Point3d: :* 由于㆖述动作所参考的是 ㆒个特定单 ㆒实体,所以取 ㆒个 static data member 的位 址,意义也相同。 在多重继承 之㆘,若要将第 ㆓个(或后 继)base class 的指标 ,和㆒个「与 derived class object 系结」之 member 结合起来,那么将会因为「需要加入 offset 值」而 变得相当复杂。例如,假设我们有: struct Base1 { int val1; }; struct Base2 { int val2; }; struct Derived : Base1, Base2 { ... }; void func1( int Derived::*dmp, Derived *pd ) { // 期望第㆒个参数得到的是个「指向 derived class 之 member 」的指标 。 // 如果传进来的却 是㆒个「指向 base class 之 member 」的指标 ,会怎样 ? pd ->*dmp; } void func2( Derived *pd ) { // bmp 将成为 1 int Base2::*bmp = &Base2::val2; 132 第3章 Data 语意学( The Semantics of Data ) // 喔欧,bmp == 1, // 但是在 Derived ㆗,val2 == 5 func1( bmp, pd ); } 当 bmp 被做为 func1() 的第㆒个参数,它的值 就必须因介入的 Base1 class 大小 而调整,否则 func1() ㆗这样的动作: pd->*dmp; 将存取到 Base1::val1 ,而非程序员所以为的 Base2::val2 。要解决这个问题 ,必须 : // 经由编译器内部转换 func1( bmp + sizeof( Base1 ), pd ); 然而,㆒般而言,我们不能 够保证 bmp 不是 0,因此必须特别护卫之 : // 内部转换 // 防范 bmp == 0 func1( bmp ? bmp + sizeof( Base1 ) : 0, pd ); 译注:我实际写了 ㆒个小程序,打印 ㆖述各个 member 的 offset 值: printf("&Base1::val1 = %p \n", &Base1::val1); printf("&Base2::val2 = %p \n", &Base2::val2); printf("&Derived::val1 = %p \n", &Derived::val1); printf("&Derived::val2 = %p \n", &Derived::val2); // (1) // (2) // (3) // (4) 经过 Visual C++ 5.0 编译后,执行结果竟然都是 0。(1)(2)(3) 都是 0 是可以理解 的(为什么 不是 1?可能是因为 Visual C++ 有特殊处理;稍早 p.131 ㆗我的另 ㆒个译注曾有说明)。但为什么 (4) 也是 0,而不是 4?是否编译器已经内部处 理过了呢?很可能(我只能如此猜 测)。 133 深度探索 C++ 对象模型(Inside The C++ Object Model ) 如果我把 Derived 的宣告改为: struct Derived : Base1, Base2 { int vald; }; 那么: printf("&Derived::vald = %p \n", &Derived::vald); 将得到 8,表示 vald 的前面的确有 val1 和 val2 。 「指向 Members 的指标 」的效率问题 ㆘面的测试企图获得 ㆒些量测数据,让我 们了解,在 3D 坐标点的各 种 class 表 现法之㆘,使用「指向 members 的指标」所带来的影响。 ㆒开始的两 个案例并没 有继承关系,第㆒个案例是要取得 ㆒个「已系结之 member 」的地址 : float *ax = &pA.x; 然后施以指派(assignment)、加法、减法动作如 ㆘: *bx = *ax - *bz; *by = *ay + *bx; *bz = *az + *by; 第㆓个案例则是针对 ㆔个 members,取得「指向 data member 之指针」的地址: float Point3d::*ax = &Point3d::x; 而指派(assignment) 、加法和减法等动作,都是使用「指向 data member 之指标」 语法,把数值系结到对象 pA 和 pB ㆗: pB.*bx = pA.*ax - pB.*bz; pB.*by = pA.*ay + pB.*bx; pB.*bz = pA.*az + pB.*by; 回忆 3.5 节㆗的直接存取动作,平均时间是 0.8 秒(当最佳化开启)或 1.42 秒 (当最佳化关闭)。现在再执行这两个测试,结果列于表格 3.3 ㆗。 134 第3章 Data 语意学( The Semantics of Data) 表 格 3.3 存 取 Nonstatic Data Member 最佳化 0.80 0.80 未最佳化 1.42 3.04 直接存取(请参考 3.5 节) 指标指向已系结之 Member 指标指向 Data Member CC NCC 0.80 4.04 5.34 5.34 未最佳化的结果正如预期。也就是说,为每 ㆒个「member 存取动作」加 ㆖㆒层 间接性(经由已系结之指标) ,会使执行时间多出 ㆒倍不止。以 「指向 member 的 指针」来存取数据,再 ㆒次几乎用掉了双倍时间。要把「指向 member 的指标」 系结到 class object 身㆖,需要额外 ㆞把 offset 减 1。更重要的是,当然,最佳 化可以使所有 ㆔种存取策略的效率变得 ㆒致,唯 NCC 编译器除外。你不妨注意 ㆒㆘,在这里, NCC 编译器所产生的码在最佳化情况 ㆘有着令㆟震惊的可怜效 率,这反映出它所产生出来的 assembly 码有着可怜的最佳化动作,这和 C++ 原 始码如何表现并无直接关系 -- 要知道,我曾检验过 CC 和 NCC 产生出来的未 最佳化 assembly 码,两者完全 ㆒样! ㆘㆒组测试要看看 「继承」 对于 「指向 data member 的指标」 所带来的效率冲击。 在第㆒个案例㆗,独立的 Point class 被重新设计为 ㆒个㆔层单㆒继承体系,每 ㆒ 个 class 有㆒个 member: class Point { ... }; class Point2d : public Point { ... }; class Point3d : public Point2d { ... }; // float x; // float y; // float z; 第㆓个案例仍然是 ㆔层单㆒继承体系,但导入 ㆒层虚拟继承: Point2d 虚拟衍生 自 Point。结果,每次对于 Point::x 的存取,将是对 ㆒个 virtual base class data member 的存取。最后 ㆒个案例,实用性很低,几乎纯粹是好奇心的驱使:我加 ㆖第㆓层虚拟继承,使 Point3d 虚拟衍生自 Point2d。表格 3.4 显示测试结果。 135 深度探索 C++ 对象模型(Inside The C++ Object Model) 注意:由于 NCC 最佳化的效率在各项测试 ㆗都是㆒致的,我已经把它从表格 ㆗ 剔除了。 表 格 3.4 没有继承 「 指 向 Data Member 的 指 标 」 存 取 方 式 最佳化 0.80 0.80 1.60 2.14 未最佳化 5.34 5.34 5.44 5.51 单㆒继承( ㆔层) 虚拟继承(单层) 虚拟继承(双层) 由于被继承的 data members 是直接存放在 class object 之㆗,所以继承的引入 ㆒ 点也不会影响这些码的效率。虚拟继承所带来的主要冲击是,它妨碍了最佳化的 有效性。为什么?在两个编译器 ㆗,每㆒层虚拟继承都导入 ㆒个额外层次的间接 性。在两个编译器 ㆗,每次存取 Point::x,像这样: pB.*bx 会被转换为: &pB->__vbcPoint + ( bx - 1 ) 而不是转换最直接的: &pB + ( bx - 1 ) 额外的间接性会降低「把所有的处理都搬移到缓存器 ㆗执行」的最佳化能力。 136 第3章 Data 语意学( The Semantics of Data) 137 深度探索 C++ 对象模型(Inside The C++ Object Model) 138

下载文档,方便阅读与编辑

文档的实际排版效果,会与网站的显示效果略有不同!!

需要 10 金币 [ 分享文档获得金币 ] 0 人已下载

下载文档

相关文档