3.3 Data Member的存取
1. 不管什么情况,每一个static data member只有一个实体,放在程序的data segment之中,每次程序取用static member,不管是通过operator::还是member selection operator,都会被内部转化为对该唯一extern实体的直接参考操作。每一个static member的存取以及与class的关联不会导致任何执行时间或空间上的额外负担。如果有两个classes,每一个都声明了一个static member freeList,那么当它们都放在程序的data segment时,就会导致名称冲突,编译器的解决方法是使用name-mangling,暗中对每一个static data member编码,以获得一个独一无二的程序识别代码。
2. 有多少个编译器,就有多少种name-mangling做法,任何name-mangling做法都有两个要点:
ü 一种算法,推导出独一无二的名称;
ü 如果编译系统或者环境工具必须和使用者交谈,那些独一无二的名称可被轻易推导回原先的名称。
3. 取一个static data member的地址,会得到一个指向其数据类型的常量指针,而不是指向其class member的指针。
4. nonstatic data members直接放在每一个class object之中,除非经过显示的explicit或隐含的implicit class object,没有办法直接存取它们。只要程序员在一个member function中直接处理一个nonstatic data member,所谓implicit class object就会发生,其实质是编译器会为这个member function增添一个const this指针,而在函数体内通过这个this指针来存取nontatic data member。
5. 欲对一个nonstatic data member进行存取操作,编译器需要把class object的起始地址加上data member的编译量offset,如地址&someObject.someMember等于&someobject + (&theClass::someMember – 1);指向data member的指针,其offset值总是会被加上1,这样可以使编译系统区分出一个指向class第一个data member的指针和一个没有指向任何data member的指针。
6. 每一个nonstatic data member的偏移量在编译时期即可获知,甚至如果member属于一个单一或多重继承体系中base class subobject也是一样,因此其存取效率和一个C struct member或一个nonderived class的member的存取效率是一样的。但是在虚拟继承的情况下就另当别论了:如果该nonstatic data member是一个virtual base class的member,并且通过指针来存取的话,在编译时期就不会得知这个member真正的offset位置,所以这个存取操作必须延迟至执行期,经由一个额外的间接导引才能够解决。
2002-7-7
3.4 “继承”与Data Member
1. 在C++继承模型中,一个derived class object所表现出来的东西,是其自己的members加上其base classes members的总和。C++并未规定derived class members和base classes members的排列次序。不过,在大部分编译器上,除virtual base class外,base class members总是先出现。
2. 一般而言,具体继承concrete inheritance并不会增加空间或存取时间上的额外负担。
3. 把两个原本独立不相干的classes凑成一对type/subtype,并带有继承关系容易犯两个错误。一是可能会重复设计一些相同操作的函数,一般而言,选择某些函数做成inline函数,是设计class的一个重要课题;二是把一个class分解为多层,有可能会为了表现class体系之抽象化,因为编译器的边界调整而膨胀所需空间。其根本原因是C++保证出现在derived class中的base class subobject有其完整原样性。
4. C++最初问世时,许多编译器把vptr放在class object的尾端,这样可以保留base class C struct的对象布局。此后,某些编译器开始把vptr放在class object的开始处,这样会给多重继承下通过指向class members之指针调用virtual function带来一些帮助,否则,在执行期不仅必须备妥从class object起点处开始量起的offset,而且必须备妥class vptr之间的offset。
5. 单一继承提供了一种自然多态的形态,是关于class体系中base type和derived type之间的转换。一般来说,base class和derived class objects都是从相同的地址开始。但若将vptr放在class object的起始处,如果base class没有virtual function而derived class有,那么单一继承的自然多态就会打破。此时,把一个derived object转换为其base类型就需要编译器的介入,用以调整地址。而在既是多重继承又是虚拟继承的情况下,编译器的介入则更有必要。
6. 多重继承的复杂度在于derived class和其上一个base class乃至上上一个base class之间的非自然关系,其主要问题发生在derived class objects和其第二或后继的base class objects之间的转换。对一个多重派生对象,将其地址指定给最左端base class的指针,情况将和单一继承相同,而第二个或后继的base class的地址指定操作则需要修改地址,加上或减去(若是downcast)介于中间的base class subobjects的大小。C++并未要求多重继承时derived class object中各个base class subjectes的排列次序,目前各个编译器都是根据声明次序来排列它们。
7. class内如果内含一个或多个virtual bass class subobjects,将被分割为两部分:一个不变局部和一个共享局部。不变局部总是拥有固定的offset,其数据用以指定共享局部的位置,可以直接存取;而共享局部表现的就是virtual base class subobject,其位置会因为每次的派生操作而变化,只可间接存取。各家编译器实现技术之间的差异就在于间接存取的方法不同。
8. 一般而言,virtual base class最有效的一种运用方式是:一个没有任何data member的抽象class。
2002-7-14
3.5 对象成员的效率
如果没有把优化开关打开,就很难猜测一个程序的效率表现,因为程序代码潜在性的受到某些与编译器有关的东西的影响。程序员如果关心效率,应该实际测试,不要光凭推论或常识判断或假设。优化操作并不一定总是能够有效运行。
2002-7-15
3.6 指向Data Members的指针
指向data members的指针可用来详细调查class members的底层布局,可用来决定vptr是放在class的起始处还是尾端,还可用来决定class中access sections的次序。
取一个nonstatic data member的地址,将会得到它在class的offset;而取一个static data member的地址或者取一个绑定于真正class object身上的data member的地址,将会得到该member在内存中的真正地址。这也正是someType someClass::*和someTye *潜在的区别。
2002-7-16
Function语意学 The Semantics of Function
C++支持三种类型的member functions:static、nonstatic和virtual,每一种类型的调用方式都不同。
4.1 Members的各种调用方式
1. C++的设计准则之一便是nonstatic member function至少必须和一般的nonmember function有着相同的效率。编译器内部会将member函数实体转换为对等的nonmember函数实体,其步骤为:
ü 改写函数原型signature以安插一个额外的参数this到member function中,使得class object可以调用该函数。其中,this是const指针,若该函数为const,则反映在this上面的结果是this指向的data也为const;
ü 将每一个对nonstatic data member的存取操作改为经由this指针来存取;
ü 将member function重新写成一个外部函数,对函数名称进行mangling处理;
此后,每一个函数调用操作也都必须转换,用以提供相应的实参。
2. 关于虚拟函数的内部转换步骤:若normalize是一个virtual member function,ptr->normalize();会被内部转化为(*ptr->vptr[t])(ptr); 事实上,vptr名称也会被mangled,因为可能存在有多个vptrs;t是vitrual table slot的索引值,关联到normalize函数;第二个ptr表示this指针。
3. 使用class scope operator明确调用一个vitual function,或经由一个class object调用一个vitual function其决议方式会和nontatic member function一样!故virtual function的一个inline函数实体可被扩展开来,因而提供极大的效率利益。
4. static member function的主要特征是没有this指针,这导致它不能直接存取其class中的nonstatic members,不能被声明为const、volatile或virtual,也不需要经由class object才能调用。static member function会被提出于class声明之外,并给予一个经过mangled的适当名称。如果取一个static member function的地址,得到的将是其在内存中的地址,其地址类型并不是一个指向class member function的指针,而是一个nonmember函数指针。static member function的一个意想不到的好处是可以成为一个callback函数,也可以成功地应用在thread函数身上。
2002-07-17
4.2 Virtual Member Functions虚拟成员函数
1. C++中,多态polymorphism表示以一个public base class指针或reference寻址出一个derived class object。识别一个class是否支持多态,唯一适当的方法试看它是否有任何virtual function。只要class拥有一个virtual function,它就需要一份额外的执行期型别判断信息。
2. 一个class只会有一个virtual table,其中内含对应class object中所有的active virtual functions的函数实体的地址。这些active virtual functions包括:
ü 一个class定义的函数实体。它会改写overriding一个可能存在的base class virtual function。
ü 继承自base class的函数实体。此时该class不改写base class virtual function。
ü 一个pure_virtual_called()函数实体,它既可以扮演pure virtual function的空间保卫者,也可以当作执行期异常处理函数。如果该函数被调用,通常的操作是结束程序。
3. 每一个virtual function都被指派一个固定不变的索引值,该值在整个继承体系中保持与特定virtual function的关联。这样就可以在编译时期设定virtual function的调用。
2002-7-20
4. 多重继承下,一个上层basse classes数目为n的derived class,它将内含n-1个额外的virtual tables。其主要实体与最左端的base class共享,其中包含所有virtual functios的地址;n-1个次要实体与其它base classes有关,其中只包含出现在对应base class中virtual functions的地址。
5. 在多重继承中支持virtual function,其复杂度围绕在第二个及后继base class上,以及执行期this指针调整上。第二(或后继)base class会影响对virtual function支持的3种情况:
ü 通过指向第二个base class的指针,调用derived class virtual function;
ü 通过指向derived class的指针,调用第二个base class中一个继承而来的virtual function;
ü 允许virtual function函数的返回值类型有所变化,可能是base type,也可能是publicly derived type。
6. 关于执行期this指针调整比较有效率的解决方法是thunk。所谓thunk是一小端assembly码,用来以适当的offset值来调整this指针并跳到相应的virtual function。thunk技术允许virtual table slot继续内含一个简单的指针,此时多重继承将不需要任何空间上的额外负担!slots中的地址可以直接指向virtual function,也可以指向一个相关的thunk。
4.3 函数的效能
nonmember、static member和nonstatic member function在内部都会转化为完全相同的形式,三者效率相同。
2002-08-08
4.4 指向Member Function的指针
对一个nonstatic member function取址,得到的是该函数在内存中的地址;而面对一个virtual function,得到的将是一个索引值。这个值是不完整的,必须被绑定于一个class object上,才能够通过它调用函数。指向member function的指针的声明语法,以及指向member selection运算符的指针,其作用是作为this指针的空间保留者。因此,static member function的类型是函数指针,而不是指向member function的指针。
使用一个member function指针,如果并不用于virtual function、多重继承、virtual base class等情况的话,其成本并不比使用一个nonmember function指针要高。
4.5 Inline Functions
关键词inline只是一项请求。如果在某个层次上,函数的执行成本比一般的函数调用及返回机制所带来的负荷低,那么该请求被接受,编译器就用一个表达式合理地将函数扩展开来。真正的inline函数扩展操作是在函数调用的那一点上。在inline扩展期间,每一个形式参数会被对应的实际参数所取代,inline函数中的每一个局部变量都必须被放在函数调用的一个封闭区段中,并拥有一个独一无二的名称。这会带来参数的求值操作以及临时性对象的管理。
2002-08-11
构造、解构、拷贝语意学 Semantics of Construction, Destruction, and Copy
1. 一般而言,class的data member应该被初始化,而且只在constructor中或其它member functions中初始化,其它任何操作都将破坏其封装性质,使其维护和修改更加困难。
2. 可以定义并调用invoke一个pure virtual function,但它只能被静态调用,不能经由虚拟机制调用。每一个derived class destructor会被编译器加以扩展,静态调用每一个virtual base class以及上一层base class的destructor。因此,不管base class的virtual destructor是否声明为pure,它必须被定义。
5.1 无继承情况下的对象构造
C++ Standard要求编译器尽量延迟nontrivial members的实际合成操作,直到真正遇到其使用场所为止。
5.2 继承体系下的对象构造
一般而言,继承体系下编译器对constructor所作的扩充操作以及次序大约如下:
ü 所有virtual base class constructors必须从左到右、从深到浅被调用:如果class被列于member initialization list中,那么任何明确指定的参数都必须传递过去,否则如果class有一个default constructor,也应该调用它;class中的每一个virtual base class subobject的偏移量offset必须在执行期可被存取;如果class object是最底层most-derived的class,其constructors可能被调用,某些用以支持这个行为的机制必须被方进来。
ü 以base class的声明次序调用上一层base class constructors:如果base class被列于member initialization list中,那么任何明确指定的参数都必须传递过去,否则若它有default constructor或default memberwise copy constructor,那么就调用它;如果base class是多重继承下的第二或后继的base class,那么this指针必须有所调整。
ü 如果class object有virtual table pointer(s),它(们)必须被设定初值,指向适当的virtual table(s)。
ü 如果有一个member没有出现在member initialization list中,但它有default constructor,调用之。
ü 将member initialization list中的data members的初始化操作以members的声明次序放进constructor的函数本身。
2002-8-18
5.3对象复制语意学 Object Copy Semantics
1. 只有在默认行为所导致的语意不安全或者不正确以致发生别名化aliasing或者内存泄漏memory leak时,才需要设计一个copy assignment operator。否则,程序反倒会执行得较慢。
2. 如果仅仅是为了把NRV优化开关打开而提供一个copy constructor,那么就没有必要一定要提供一个copy assignment operator。
3. copy assignment operator有一个非正交情况,那就是它缺乏一个平行于member initialization list的member assignment list。调用base class的copy assignment operator示例:
Point::operator = (p3d); 或 (*(Point*)this) = p3d; 或 (Point &)(*this) = p3d;
4. 事实上,copy assignment operator在虚拟继承情况下行为不佳,需要小心设计和说明。许多编译器甚至并不尝试取得正确的语意,它们在每一个中间的copy assignment operator中调用每一个base class instance,于是造成virtual base copy assignment operator的多个实体被调用。建议尽可能不要允许一个virtual base class的拷贝操作,并不要在任何virtual base class中声明data member。
5.5解构语意学 Semantics of Destruction
如果class没有定义destructor,那么只有在其内带的member object或base class拥有destructor时,编译器才会自动合成出一个destructor。一个由程序员定义的destructor被扩展的方式类似constructors被扩展的方式,只是顺序相反:
ü destructor的函数本体首先被执行;
ü 如果class拥有member class objects,而后者拥有destructors,那么它们将以声明的相反顺序而调用;
ü 如果object内带一个vptr,则现在被重新设定以指向适当base class之virtual table;
ü 如果有任何直接的nonvirtual base classes拥有destructor,它们将以声明的相反顺序而调用;
ü 如果有任何virtual base classes拥有destructor,而前面讨论的这个class是most-derived class,那么它们会以原先构造顺序的相反顺序被调用。
2002-8-19
执行期语意学 Runtime Semantics
6.1对象的构造和解构
1. 一般而言,constructor和destructor的安插都如你所预期。但如果一个区段或函数中有一个以上的离开点,情况就会复杂一些,destructor会放在每一个离开点之前。通常,我们要求将object尽可能放在使用它的那个程序区附近,这样做可以节省不必要的对象产生和销毁操作。
2. C++程序中所有的global objects都被放置在程序的data segment中,如果不明确指定初值,object所配置的内存内容将为0(C并不自动设定初值)。如果global object有constructor和destructor的话,我们说它需要静态的初始化和内存释放操作。
2002-8-20
3. virtual base class的subobject在每个derived class中的位置可能会变动,不能在编译时期确定。以一个derived class的pointer或reference来存取virtual base class subobject,是一种nonconstant expression,必须在执行期方可评估求值。
4. 使用静态初始化的object有一些缺点。其一,无法放入try区段,任何throw操作必将触发exception handling library的默认函数terminate();其二,程序员必须为控制“需要跨越模块做静态初始化”objects的依赖顺序而产生的复杂度付出代价。建议根本就不要使用那些需要静态初始化的global objects。
5. 新的C++标准要求编译单位中的static local class objects必须在相应函数第一次被调用时才被构造,而且必须以相反的次序销毁。由于这些objects是在需要时才被构造,因此编译时期无法预期其集合和顺序。为支持新标准,可能要对被产生出来的static local class objects保持一个执行期链表。
2003-8-1
6. 对于对象数组定义,晚近的编译器一般会提供两个函数,分别用于处理没有virtual base class的class,以及内带virtual base class的class ,它们通常被称为vec_new、vec_vnew。前者类型通常为:
void* vec_new( // 初始化程序员未提供初值的连续元素
void *array, // 数组起始地址若为0,则动态分配
size_t elem_size, // 每一个class object的大小
int elem_count, // 数组中的元素数目
void (*constructor) (void *), // class的default constructor指针
void (*destructor) (void *, char) // class的destructor指针,以0填入
); 如果程序员提供带有默认参数值的default constructor,编译器要做特殊处理,以传入默认参数值!
对应销毁数组的两个函数分别为vec_delete、vec_vdelete。前者类型通常为:
void* vec_delete(
void *array, // 数组起始地址
size_t elem_size, // 每一个class object的大小
int elem_count, // 数组中的元素数目
void (*destructor) (void *, char) // class的destructor指针
);
6.2 new和delete运算符
注意区分operator new和new operator!前者负责分配内存;后者先调用前者分配内存,然后调用constructor以实施初始化。
完