前 言 Stanley B.Lippman
1. 任何对象模型都需要的三种转换风味:
ü 与编译器息息相关的转换
ü 语言语义转换
ü 程序代码和对象模型的转换
2. C++对象模型的两种解释
ü 语言中直接支持面向对象程序设计的部分
ü 对于各种支持的底层实现机制
3. C++ class的完整virtual functions在编译时期就固定下来了,程序员没有办法在执行期动态增加或取代其中某一个。这使得虚拟函数调用操作得以有快速的派送结果,付出的却是执行期的弹性。
4. 目前所有编译器对于virtual function的实现都是使用各个class专属的virtual table,大小固定,并且在程序执行前就构造好了。
5. C++对象模型的底层机制并未标准化,它会因实现品(编译器)和时间的变动而不同。
2002-6-23
关于对象 Object Lessons
1.1 C++对象模式
1. C++在布局以及存取时间上主要的额外负担是由virtual引起的,包括virtual function机制和virtual base class 机制,还有一些发生在“一个derived class和其第二或后继之base class的转换”上的多重继承。
2. 在C++对象模型中,nonstatic data members被配置于每一个class object之内,static data members则被存放在所有的class object之外,static和nonstatic function members也被放在所有的class object之外,virtual functions则以两个步骤支持:每个class产生一堆指向virtual functions的指针,放在virtual table (vtbl)中;每个class object被添加一个指针vptr,指向相关的virtual table。每个class所关联的type_info object也经由vtbl指出,通常是放在vtbl的第一个slot处。vptr由每一个class的construtor、destructor以及copy assignment operator自动完成。以上模型的主要优点在于空间和存取时间的效率,主要缺点是,只要应用程序所用到的class object的nonstatic data members有所修改,那么应用程序代码就必须重新编译。
3. C++最初所采用的继承模型并不运用任何间接性,base class subobject的data members直接放置于derived class object中。优点是提供对base class members紧凑且高效的存取,缺点是base class members的任何改变,都将导致使用其derived class 的object的应用程序代码必须重新编译。
4. virtual base class的原始模型是在class object中为每一个有关联的virtual base class加上一个指针,其他演化出来的模型不是导入一个virtual base class table,就是扩充原已存在的vtbl,用以维护每一个virtual base class的位置。
1.2关键词所带来的差异
1. 可以说关键词struct的使用伴随着一个public接口的声明,也可以说它的用途只是为了方便C程序员迁徙至C++部落。
2. C++中凡处于同一个access section的数据,必定保证以声明次序出现在内存布局中,然而被放在多个access sections中的各笔数据排列次序就不一定了。同样,base classes和derived classes的data members的布局也没有谁先谁后的强制规定。
3. 组合composition而非继承才是把C和C++结合在一起的唯一可行方法。
1.3对象的差异
1. C++程序设计模型支持三种程序设计典范programming paradigms:
ü 程序模型procedural model
ü 抽象数据类型模型abstract data type model, ADT
ü 面向对象数据模型object-oriented model,OO
2. 虽然可以直接或间接处理继承体系中的一个base class object,但只有通过pointer或reference的间接处理,才能支持OO程序设计所需的多态性质。
3. C++中,多态只存在于public class体系中,nonpublic的派生行为以及类型为void*的指针可以说是多态,但它们没有被语言明白地支持,必须由程序员通过显示的转型操作来管理。
4. C++以下列方法支持多态:
ü 经由一组隐含的转化操作,如把一个derived class指针转化为一个指向其public base type的指针;
ü 经由虚拟机制;
ü 经由dynamic_cast和typeid运算符。
5. 多态的主要用途是经由一个共同的接口来影响类型的封装,这个接口通常被定义在一个抽象的base class中。这个接口是以virtual function机制引发的,它可以在执行期根据object的真正类型解析出到底是哪一个函数实体被调用。
6. 一个class object所需的内存,一般而言由以下部分组成:
ü nonstatic data members的总和大小;
ü 任何由于alignment需求而填补上去的空间;
ü 为支持virtual而由内部产生的任何额外负担。
7. 一个pointer或reference,不管它指向哪一种数据类型,指针本身所需的内存大小是固定的。本质上,一个reference通常是以一个指针来实现,而object语法如果转换为间接手法,就需要一个指针。
8. 指向不同类型之指针的差异,既不在其指针表示法不同,也不在其内容不同,而是在其所寻址出来的object类型不同,亦即指针类型会教导编译器如何解释某个特定地址中的内存内容及大小。它们之所以支持多态,是因为它们并不引发内存中任何与类型有关的内存委托操作,会受到改变的只是它们所指向的内存的大小和内容的解释方式。
9. 转型cast操作其实是一种编译指令,大部分情况下它并不改变一个指针所含的真正地址,它只是影响被指向之内存的大小和内容的解释方式。
10.一个base class object被直接初始化或指定为一个derived object时,derived object就会被切割sliced,以塞入较小的base type内存中,多态于是不再呈现。一个严格的编译器可以在编译时期解析一个通过该object而触发的virtual function调用操作,从而回避virtual机制。这时,如果virtual function被定义为inline,则会有效率上的收获。
11.C++通过class的pointer和reference来支持多态,这种程序设计风格就是所谓的OO;C++也支持具体的ADT程序风格,如今被称为object-based OB,不支持多态,不支持类型的扩充。
2002-6-25
构造函数语意学The Semantics of Constructors
1. Jerry Schwarz,iostream函数库建构师,曾为了让cin能够求得一个真假值,于是他为它定义了一个conversion运算符operator int()。但在语句cin << intVal中,其行为出乎意料:程序原本要的是cout而不是cin!但是编译器却找到一个正确的诠释:将cin转型为整型,现在left shift operator <<就可以工作了!这就是所谓的“Schwarz Error”。Jerry最后以operator void *()取代operator int()。
2. 引入关键词explicit的目的,就是为了提供程序员一种方法,使他们能够制止单一参数的constructor被当作一个conversion运算符。其引入是明智的,但其测试应该是残酷的!
2.1 Default Constructor的建构操作
1. global objects的内存保证会在程序激活的时候被清为0。local objects配置于程序的堆栈中,heap objects配置于自由空间中,都不一定会被清为0,它们的内容将是内存上次被使用后的遗迹。
2. 在各个不同的版本模块中,编译器避免合成出多个default constructor的方法:把合成的default constructor、copy constructor、assignment copy operator都以inline方式完成。一个inline函数有静态链接,不会被档案以外者看到。如果函数过于复杂,不适合做成inline,就会合成一个explicit non-inline static实体。
3. 以下四种情况,编译器必须为未声明constructor的classes合成一个implicit nontrivial default constructor:带有default constructor的member class object,带有default constructor的base class,带有virtual function,带有virtual base class。其它各种情况且没有声明任何constructor的classes,它们拥有的是implicit trival default constructors,它们实际上并不会被合成出来。
4. 编译器合成implicit nontrivial default constructor,不过是暗地里作了一些重要的事情以保证程序正确合理地运行。如果程序员提供了多个constructors,但其中都没有default constructor,编译器同样会在这些constructors中插入一些相同功能的代码,这些代码都将被安插在explicit user code之前。
2002-6-26
2.2 Copy Constructor的建构操作
1. 有三种情况,会以一个class的内容作为另一个class object的初值:
ü 对一个object作明确的初始化操作,如:someClass obt = obtb;
ü 一个object被当作参数交给某个函数时,如:foo(obt);
ü 当函数返回一个class object时。
若class设计者明确定义了一个copy constructor,大部分情况下,该constructor会被调用。这可能导致一个暂时性的class object的产生或程序代码的蜕变,或者两者皆有。
2. 如果class没有提供一个explicit copy constructor,当class object以相同class的另一个object作为初值时,其内部是以所谓的default memberwise initialization手法完成的,即把每一个内建的或派生的data member的值,从一个object拷贝到另一个object。不过,它并不会拷贝其中的member class object,而是以递归的方式施行memberwise initialization。
3. 一个class object可以从两种方式复制得到:初始化和指定,从概念上而言,这两个操作分别是以copy constructor和copy assignment operator完成的。
4. 如果class没有声明一个copy constructor,就会有隐含的声明implicitly declared或隐含的定义implicitly defined出现。C++把copy constructor分为trivial和nontrivial两种。只有nontrivial的实体才会被合成出来。决定一个copy constructor是否为trivial的标准在于class是否展现出所谓的“bitwise copy semantics”。
5. 以下四种情况,一个class不展现bitwise copy semantics:
ü class内含一个member object而后者的class声明有或被编译器合成有一个copy constructor时;
ü class继承自一个base class而后者存在或被编译器合成有一个copy constructor时;
ü 当class声明了一个或多个virtual functions时;
ü 当class派生自一个继承串链,其中有一个或多个virtual base classes时。
前两种情况中,编译器必须将member或base class的copy constructors调用操作安插到被合成的copy constructor中。
6. 一旦一个class object中必须引入vptr,编译器就必须为它的vptr正确地设置好初值。此时,该class就不再展现bitwise semantics。
7. 当一个base class object以其derived class object内容作初始化操作时,其vptr复制操作必须保证安全。
8. 每一个编译器对于虚拟继承的承诺,都表示必须让derived class object中的virtual base class subobject的位置在执行期准备妥当。维护位置的完整性是编译器的责任。
2002-6-27
2.3 程序转化语意学
1. 每一个明确的初始化操作都会有两个必要的程序转化阶段:先重写每一个定义,剥除其中的初始化操作,然后安插class的copy constructor调用操作。
2. 把一个class object当作参数传给一个函数或是作为一个函数的返回值,相当于以下形式的初始化操作:
X xx = arg; 其中xx代表形式参数或返回值,而arg代表真正的参数值。
3. 函数定义如下:X bar(){X xx; return xx;},bar()的返回值通过一个双阶转化从局部对象xx中拷贝出来:
ü 首先为bar添加一个额外参数,类型是class object的一个reference,这个参数用来放置被拷贝构建而得的返回值。
ü 然后在return指令之前安插一个copy constructor调用操作,以便将欲传回之object的内容当作上述新增参数的初值,同时重写函数使它不返回任何值。
4. Named Return Value(NRV)优化如今被视为是标准C++编译器的一个义不容辞的优化操作,它的特点是直接操作新添加的额外参数。注意只有copy constructor的出现才会激活C++编译器的NRV优化!NRV优化虽然极大地改善了效率,但还是饱受批评:一是优化由编译器默默完成,而是否完成以及其完成程度完全透明;二是一旦函数变得比较复杂,优化就变得较难施行;三是优化由可能使程序产生错误——有时并不是对称地调用constructor和destructor,而是copy constructor未被调用!
5. 在编译器提供NRV优化的前提下,如果可以预见class需要大量的memberwise初始化操作,比如以by value的方式传回objects,那么提供一个explicit inline copy constructor的函数实体就非常合理。此种情况下,没有必要同时提供explicit assignment operator定义。
6. copy constructor的应用迫使编译器多多少少对程序代码作部分优化,尤其是当一个函数以by value的方式传回一个class object,而该class有一个copy constructor(或定义或合成)时,无论在函数的定义还是在使用上将导致深奥的程序转化。此外,编译器将实施NRV优化。
7. 注意正确使用memset()和memcpy(),它们都只有在classes不含任何由编译器产生的内部members如vptr时才能有效运行!
2002-6-30
2.4 成员初始化列表
1. 当写下一个constructor时,就有机会设定class members的初值。不是经由member initialization list,就是在constructor函数本身之内。
2. 下列情况,为了让程序能被顺利编译,必须使用member initialization list:
ü 初始化一个reference member时;
ü 初始化一个const member时;
ü 调用一个base class的constructor,而它拥有一组参数时;
ü 调用一个member class的constructor,而它拥有一组参数时。
3. 编译器会对initialization list一一处理并可能重新排序,以反映出members的声明次序,它会安插一些代码到constructor内,并置于任何explicit user code之前。
4. 一个忠告:请使用“存在于constructor体内的一个member”,而不是“存在于member initialization list中的一个member”,来为另一个member设定初值。
2002-7-1
Data语意学 The Semantics of Data
讨论如下继承体系:
class X{};
class Y : public virtual X{};
class Z : public virtual X{};
class A: public Y, public Z{};
1. 一个empty class如class X{},它有一个隐晦的1 byte,那是被编译器安插进去的一个char,使得这个class的两个objects得以在内存中配置独一无二的地址。
2. Y和Z的大小受到三个因素的影响:
ü 语言本身所造成的额外负担overhead。语言支持virtual base classes时导致的额外负担反映在某种形式的指针身上,它要么指向virtual base class subobject,要么指向一个存放virtual base class subobject地址或者其偏移量offset的表格。
ü 编译器对于特殊情况所提供的优化处理。virtual base class X 1 byte大小的subobject也出现在class Y和Z身上。传统上它被放在derived class的固定部分的尾端。某些编译器对empty virtual base提供特殊处理,将它视为derived class object最开头的一部分,它不用会任何的额外空间,也就是前面提到的1 byte。
ü Alignment的限制。Alignment就是将数值调整到某数的整数倍,在32位计算机上,通常该数为4 bytes(32位),以使bus的运输量达到最高效率。
3. 一个virtual base class subobject只会在derived class中存在一份实体,不管它在class继承体系中出现了多少次,class A的大小由下列几点决定:
ü 被大家共享的唯一一个class X实体,大小为1 byte;
ü Base Y、Z的大小减去因virual base class而配置的大小;
ü class A自己的大小;
ü class A的alignment数量。
4. C++ standard并不强制规定base class subobjects、不同存取级别的data members的排列次序这种琐碎细节,它也不规定virtual function以及virtual base classes的实现细节。
5. C++对象模型尽量以空间优化和存取速度优化来表现nonstatic data members,并且保持和C语言struct数据配置的兼容性。它把数据直接存放在每一个class object中,对于继承而来的nonstatic data members,不管是virtual或nonvirtual base class也是如此。至于static data members则被放置在程序的一个global data segment中,不会影响个别class object的大小。static data member永远只存在一份实体,但是一个template class的static data member的行为稍有不同。
3.1 Data Member的绑定
inline member function躯体内的data member绑定操作,会在整个class声明完成后才发生,而argument list中的名称还是会在它们第一次遭遇时被适当地决议resolved完成。基于这种状况,请始终把nested type声明放在class的起始处。
2002-7-2
3.2 Data Member的布局
1. 每一个private、protected、public区段就是一个access section。C++ Standard要求,在同一个access section中,members的排列只需满足“较晚出现的members在class object中有较高的地址”这一条件即可。也就是说各个members并不一定的连续排列,alignment可能需要的bytes以及编译器可能合成供内部使用的data members都可能介于被声明的members之间。
2. C++ Standard也允许编译器将多个access sections之中的data members自由排列,不必在乎它们出现在class声明中的次序。当前各家编译器都是把一个以上的access sections连锁在一起,依照声明的次序成为一个连续区块。access sections的多寡不会导致额外负担。
3. vptr传统上会被放在所有明确声明的members的最后,不过如今也有一些编译器把vptr放在class object的最前端。
4. 一个用来判断哪一个section先出现的template function:
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”;
}
(我在VC++ 6.0下测试该函数,编译尚未通过。这个,应该怪罪编译器)