《深度探索C++对象模型(Inside The C++ Object Model )》学习笔记
转载:http://dsqiu.iteye.com/blog/1669614
第一章 关于对象
使用class封装之后的布局成本:
class并没有增加成本,data members直接内含在每一个class object之中,就像C struct一样。而member functions虽然被包含在class的声明之内,但是不出现在Object之中。每一个non-inline function 只会产生一个函数实体。至于inline function则会在每一个调用使用的地方产生一个函数实体(在调用点展开函数体)。
class在布局以及存取时间上主要的额外负担是由virtual 引起,包括:
virtual function 机制 用以支持一个有效率的“执行期绑定(runtime binding)”。
virtual base class 用以实现“多次出现在继承体系中的 base class ,有一个单一的被共享的实体”。
当然还有一些剁成继承下的额外负担,发生在“一个 derived class 和其第二或后继之 base class 的转换”之间。
C++ 对象模型
在C++对象模型中,nonstatic data members 被放置在每一个class object之内,static data members 则被存放在所以class object 之外。static和nonstaitc function也被放在所有 class object 之外。virtual functions 则以两个步骤支持之:
1.每一个 class 产生出一堆指向 virtual functions 的指针,放在表格之中,这个表格被称为 virtual table(vtbl).
2.每一个 class object被添加一个指针,指向相关的virtual table 。通常这个指针被称为 vptr ,vptr的设定和重置都由每一个 class 的 constructor 、destructor 和 copy assignment 运算符自动完成。
C++ 以下列方法支持多态:
1.经由一组隐含的转化操作。例如把一个 derived class 指针转换为一个指向其 public base type的指针。
2.经由 virtual function 机制。
3.经由dynamic_cast和typied运算符。
class object 需要多少内存:
1.其 nonstatic data members的总和大小。
2.加上任何犹豫alignment的需求和padding(填补)上去的空间。
3.加上为了支持 virtual 而由内部产生的任何额外负担。
指针类型:告诉编译器如何解释某个特定地址中的内存内容及其大小(例如:一个string是传统的8 bytes(包括一个 4byte的字符指针和一个用来表示字符串长度的整数)。转型(cast)其实是一个编译指令,大部分不会改变一个指针所含有的真正地址,它只影响“被指出值内存的大小和其内容”的解释方式。
第二章 构造函数语意学
2.1 Default Constructor
当编译器需要的时候,default constructor会被合成出来,只执行编译器所需要的任务(将members适当初始化)。
带有 Default Constructor 的Member Class Object
编译器的出来是:如果一个class A 内含一个或者一个以上 member class objects ,那么class A 的每一个 constructor 必须调用每一个member classes 的default constructor 。编译器会扩张已存在的constructors,在其中安插一些代码,使得 user code在被执行之前,先调用(调用顺序一member objects在class 的声明次序一致)必要的 default constructors。
带有 Default Constructor 的 Base class
编译器会在 Member Class Object 的default constructor 被插入调用之前,调用(调用次序根据他们的声明次序)所有 base class constructor 的default constructor 。
带有一个 Virtual Function 的Class
下面两种情况同样需要合成default constructor:
1.class 声明(或继承)一个 virtual function。
2.class派生自一个继承串链,其中一个或者更多的 virtual base class。
扩展(constructor)操作会在编译期间发生:
1.一个virtual function table 会被编译器产生出来,内放class 的virtual functions 的地址。
2.在每一个 class object 中,一个额外的pointer member(vptr)会被编译器合成出来,内含相关的class vtbl的地址。
带有一个 Virtual Base Class 的class
Virtual base class的实现法在不同编译器之间有很大差异,然而,每一个实现的共同点在于必须使 virtual base class 在其每一个 derived class object中的位置,能够在执行期准备妥当。对于class所定义的每一个constructor 编译器都会安插那些“允许每一个virtual base class 的执行期存取操作”的码。
总结
以上四种情况,会导致“编译器必须为未声明constructor 的class 合成一个default constructor ”,这只是编译器(而非程序)的需要。它之所以能够完成任务,是借着“调用member object 或base class的default constructor ”或是“为每一个object初始化其 virtual function 机制或virtual base class 机制”完成。至于没有存在这四种情况而又没有生命constructor的class 实际上是不会被合成出来的。
在合成的default constructor 中,只有base class subobjects(子对象)和member class objects会被初始化。所有其他的nonstatic data member ,如整数,整数指针,整数数组等是不会被初始化的,这些初始化操作对程序是必须的,但对编译器则并非需要的。
C++新手一般有两个误解:
1.任何class 如果没有定义default constructor ,就会被合成出来一个。
2.编译器合成出来的default constructor 会明确设定 class 内每一个data member的默认值。
2.2 Copy Constructor
有三种情况,会以一个object的内容作为另一class object的初值。
1.最明显的当然是对一个object做明确的初始化操作。
2.当object被当做参数交给某个函数
3.当函数返回一个class object。
这三种情况需要有 copy constructor。
Default Memberwise Initialization
如果class 没有提供一个 explicit copy constructor时,当class object以“相同的另一个object作为初值是,其内部是以所谓的default memberwise initialization方式完成的。也就是把每一个内建的或派生的 data member(例如一个数组或指针)的值,从某个object拷贝一份到另一个object上,但不拷贝其具体内容。例如只拷贝指针地址,不拷贝一份新的指针指向的对象,这也就是浅拷贝,不过它并不会拷贝其中member class object,而是以递归的方式实行memberwise initialization。
这种递归的memberwise initialization是如何实现的呢?
答案就是Bitwise Copy Semantics和default copy constructor。如果class展现了Bitwise Copy Semantics,则使用bitwise copy(bitwise copy semantics编译器生成的伪代码是memcpy函数),否则编译器会生成default copy constructor。
那什么情况下class不展现Bitwise Copy Semantics呢?有四种情况:
1.当class内含有一个member class object,而这个member class 内有一个默认的copy 构造函数[不论是class设计者明确声明,或者被编译器合成]
2.当class 继承自 一个base class,而base class 有copy构造函数[不论是class设计者明确声明,或者被编译器合成]
3.当一个类声明了一个或多个virtual 函数
4.当class派生自一个继承串链,其中一个或者多个virtual base class
下面我们来理解这四种情况为什么不能使用bitwise copy,以及编译器生成的copy constructor都干了些什么。
在前2种情况下,编译器必须将member或者base class的“ copy constructor的调用操作”安插到被合成的copy constructor中。
重新设定Virtual Table 的指针
第3种情况下,因为class 包含virtual function, 编译时需要做扩张操作:
1.增加virtual function table,内含有一个有作用的virtual function的地址;
2.创建一个指向virtual function table的指针,安插在class object内。
所以,编译器对于每一个新产生的class object的vptr都必须被正确地赋值,否则将跑去执行其他对象的function了,其后果是很严重的。因此,编译器导入一个vptr到class之中时,该class 就不在展现bitwise semantics,必须合成copy Constructor并将vptr适当地初始化。
处理Virtual Base Class Subobject
virtual base class的存在需要特别处理。一个class object 如果以另一个 virtual base class subobject那么也会使“bitwise copy semantics”失效。
每一个编译器对于虚拟继承的支持承诺,都是表示必须让“derived class object 中的virtual base class subobject 位置”在执行期就准备妥当,维护“位置的完整性”是编译器的责任。Bitwise copy semantics 可能会破坏这个位置,所以编译器必须自己合成出copy constructor。
这也就是说,拷贝构造函数和默认构造器一样,需要的时候会进行构建,而并非程序员不写编译器就帮着构建。
2.4 初始化列表
下面四种情况必须使用初始化列表来初始化class 的成员:
1.当初始化一个reference member时;
2.当初始化一个const member时;
3.当调用一个base class 的 constructor ,而它拥有一组参数(其实就是自定义的构造函数)时;
4.当调用一个 member class 的 constructor,而它拥有一组参数时。
不过,初始化的顺序是class members声明次序决定的,不是由初始化列表决定的。
第三章 Data 语意学
3.2 Data Member 的布局
nonstatic data members 在class object中的排列顺序将和其声明的顺序一样的。但C++ standard 允许编译器将多个access sections之中的data members自由排列,不必在乎他们的出现在class中的声明顺序。
3.3 Data Member 的存取
每一个member 的存取许可(private public protected),以及与class的关联,并不会导致任何空间上或执行时间上的额外负担——不论是在个别的class objects 或是在static data member 本身。
static data members 被视为global变量,只有一个实体,存放在程序的data segment之中,每次取static member 就会被内部转化为对该唯一的extern 实体的直接参考操作。若取一个static data member的地址,会得到一个数据类型的指针,而不是只想起class member的指针。
nonstatic data members 欲对一个nonstatic data member 进行存取操作,编译器需要吧class object的起始地址加上data member的偏移量(在编译事情就可以获知)。
3.4 继承与Data Member
只要继承不要多态
这种情况并不会增加空间或存储时间上的额外负担。这种情况base class和derived class的objects都是从相同的地址开始,其差异只在于derived object 比较大,用以容纳自建的nonstatic data members,把一个derived class object指定给base class 的指针或引用,并不需要编译器去调停或修改地址,它很滋润的可以发生,而且提供了最佳执行效率。
加上多态
这种情况会带来空间和存取时间的额外负担:
1.导入一个和virtual table ,用来存储它所声明的每一个virtual functions的地址。
2.在每一个class object中导入一个vptr,提供执行期的链接,使每一个object能够找到相应的virtual table。
3.加强constructor,使它能够为vptr设定初始值,让它指向class 所对应的virtual table 。
4.加强destructor,使它能够消抹“指向class 相关virtual table”的vptr。
多重继承
对于一个多重派生对象,将其地址指定给“最左端(第一个)base class的指针”,情况和单一继承时相同,因为二者都指向了相同的起始地址,至于第二个或后面的base class 的地址指定操作,则需要将地址修改过:加上(或减去,如果是downcast)介于中间的base class subobject(s)的大小。
如果要存取第二个(或后面)的base class 中的一个data member ,不需要付出额外的成本,因为members的位置在编译时就固定了,因此存取member只是一个简单的offset的运算。
虚拟继承
class如果含有一个或多个virtual base class subobjects将被分割为两部分:一个不变局部和一个共享局部。不变局部中的数据,总是能有固定的offset,这部分可以被直接存取,至于共享部分,所表现的就是virtual base class subobject ,这个部分数据,其位置因为每次派生操作而有变化,所以只能间接存取。
如果没有virtual functions的情况下,它们和C struct完全一样。
第四章 Function 语意学
4.1 Member的各种调用方式
Nonstatic Member Functions
实际上编译器是将member function被内化为nonmember的形式,经过下面转化步骤:
1.给函数添加额外参数——this。
2.将对每一个nonstaitc data member的存取操作改为this指针来存取。
3.将member function 重写成一个外部函数。对函数名精选mangling 处理,使之成为独一无二的语汇。
Virtual Member Functions
将
ptr->f(); //f()为virtual member function
内部转化为
(*ptr->vptr[1](ptr);
其中:
vptr表示编译器产生的指针,指向virtual table。它被安插在每一个声明有(或继承自)一个或多个virtual functions 的class object 中。
1 是virtual table slot的索引值,关联到normalize()函数。
第二个ptr表示this指针。
Static Member Functions
不能被声明为 const volatile 或virtual。
一个static member function 会提出于class声明之外,并给予一个经过mangling的适当名称。如果取一个static member function 的地址,获得的是其在内存的位置也就是地址,而不是一个指向“class member function”的指针,如下:
&Point::count();
会得到一个数值,类型是:
unsigned int(*)();
而不是:
unsigned int(Point::*)();
4.2 Virtual Member Funcitons
C++中,多态表示以“一个public base class 的指针(或reference),寻址出一个derived class object”。
每一个class 只会有一个virtual table,每一个table 含有对应的class object中所有active virtual functions 函数实体地址。这些active virtual function 包括:
1.这个class 所定义的函数实体(改写(overriding)一个可能存在的base class virtual function函数实体。
2.继承自base class 的函数实体(不被derived class改写)
3.一个pure_virtual_called()。
一个类继承函数virtual table的三种可能性:
1.继承base class 所声明的virtual functions的函数实体。正确地说,是该函数实体的地址会被拷贝到derived class的virtual table相对应的slot之中。
2.使用自己的函数实体。这表示它自己的函数实体地址必须放在对应的slot之中。
3.可以加入一个新的virtual function。这时候virtual table 的尺寸会增大一个slot放进这个函数实体地址。
编译时期设定virtual function的调用:
一般而言,我并不知道ptr 所指对象的真正类型。然而可以经由ptr 可以存取到该对象的virtual table。
虽然我不知道哪个Z()函数实体被调用,但知道每一个Z()函数地址都被放置slot 4的索引。
这样我们就可以将
ptr->z();
转化为:(*ptr->vptr[4])(ptr);
唯一一个在执行期才能知道的东西是:slot4所指的到底是哪一个z()函数实体。
多重继承下的 Virtual Functions
在多重继承中支持virtual functions,其复杂度围绕在第二个及其后面的base class 上,以及“必须在执行期调整this 指针”这一点。一般规则是,经由指向“第二或后继base class 的指针”来调用derived class virtual function。调用操作连带的“必要的this指针调整”操作,必须在执行期完成。
虚拟继承下的 Virtual Functions
4.3 函数的效能
nonmemeber、static member或nonstatic member函数都被转换为完全相同形式,所以三者效率完全相同。
4.4 指向Member Function的指针
取一个nonstatic data member的地址,得到的结果是该member在class 布局中的bytes位置,所以它需要绑定于某个class object的地址上,才能够被存取。
取一个nonstatic member function的地址,如果该函数是nonvirtual,则得到的是内存的真正地址,然后这个值也是不完全的,也需要绑定于某个class object的地址上,才能够调用函数。
支持“指向Virtual Member Function”之指针
对于一个virtual function,其地址在编译时期是未知的,所能知道的仅是virtual function在其相关之virtual table的索引值,也就是说,对于一个virtual member function 取其地址,所能获得的只是一个索引值。
4.5 Inline Funcitons
形参 传入参数,直接替换 传入常量,连替换都省了,直接变成常量 传入函数运行结果,则需要导入临时变量
局部变量 局部变量会被mangling,以便inline函数被替换后名字唯一 也就是说一次性调用N次,就会出现N个临时变量……程序的体积会暴增
第五章 构造、解构、拷贝 语意学
继承体系下的对象构造
constructor的调用伴随了哪些步骤:
1.初始化列表(member initialization list)的data members初始化操作会被放进constructor的函数本身,并以membs的声明顺序为顺序。
2.如果有一个member并没有在初始化列表中,但它在一个default constructor,那么该default constructor 必须被调用(手动)。
3.在那之前,如果class object有virtual table pointer(s),它(们)必须被设定初始值,指定适当的virtual table(s)。
4.在那之前,所有上一层的base class constructors 必须被调用,以base class 的声明顺序为顺序(与初始化列表的顺序没有关联)。
如果base class 被列于初始化列表中,那么任何明确指定参数都应该传递过去。
如果base class 没有列于初始化列表,那么调用default constructor。
如果base class 是多重继承下的第二或后面的base class ,那么this指针必须有所调整。
5.在那之前,所有 virtual base class constructors 必须被调用,从左到右,从最深到最浅。
如果class 被列于初始化列表中,那么如果有任何明确指定的参数,都应该传递过去,若没有列于初始化列表中,则调用default constructor。
此外,class中的每一个virtual base class subobject的偏移量必须在执行期可存取。
如果class object 是最底层的class,某constructors可能被调用;某些用以支持这个行为的机制必须被放进来。
对象复制语意学
当设计一个class,并以一个class object 指定另一个class object时,有三种选择:
1.什么都不做,实施默认行为。
2.提供一个explicit copy assignment operator。
3.明确拒绝一个class object指定给另一个class object。
一个class对于默认的copy assignment operator,在以下情况下不会表现出 bitwise copy语意:
1.当一个class的base class 有一个copy assignment operator时,
2.当一个class 的member object,而其class 有一个copy assignment operator时,
3.当一个class 声明了任何virtual functions时,
4.当class继承一个virtual base class 时。
vptr语意学
vptr在constructor何时被初始化?在base class constructors调用操作之后,但是在程序员供应的码或是初始化列表中所列的members初始化操作之前。
解构语意学
destructor被扩展的方式:
1.destructor的函数本身首先被执行。
2.如果class拥有member class objects,而后拥有destructor,那么它们会以声明顺序的相反顺序被调用。
3.如果object内带一个vptr,则现在被重新设定,指向适当的base class virtual table。
4.如果有任何直接的(上一层)nonvirtual base classes 拥有destructor ,它们会以声明顺序相反顺序调用。
5.如果有任何virtual base classes 拥有destructor,而当前讨论的这个class 是最尾端的class,那么它们会以其原来顺序相反顺序被调用。
第六章 执行期语意学
第七章 ……
补充:类型向上转型和多态的混淆
构造这样的一个继承体系:
class Base {
public: virtual ~Base() {}
virtual void show() { cout << "Base" << endl; }
};
class Derived : public Base {
public: void show() { cout << "Derived" << endl; }
};
子类Derived类重写了基类Base中的show方法。 编写下面的测试代码:
Base b; Derived d;
b.show(); d.show();
结果是:
Base
Derived
Base的对象调用了Base的方法,而Derived的对象调用了Derived的方法。因为直接用对象来调用成员函数时不会开启多态机制,故编译器直接根据b和d各自的类型就可以确定调用哪个show函数了,也就是在这两句调用中,编译器为它们每一个都确定了一个唯一的入口地址。这实际上类似于一个重载多态,虽然这两个show函数拥有不同的作用域。 那这样呢: Base b; Derived d; b.show(); b = d; b.show(); 现在,一个Base的对象被赋值为子类Derived的对象。
那这样呢:
Base b; Derived d;
b.show(); b = d; b.show();
现在,一个Base的对象被赋值为子类Derived的对象。
结果是:
Base
Base
对于熟悉Java的人而言,这不可理解。但实际上,C++不是Java,它更像C。“b = d”的意思,并不是Java中的“让一个指向Base类的引用指向它的子类对象”,而是“把Base类的子类对象中的Base子对象分割出来,赋值给b”。所以,只要b的类型始终是Base,那么b.show()调用的永远都是Base类中的show函数;换句话说,编译器总是把Base中的那个show函数的入口地址作为b.show()的入口地址。这根本就没用上多态。
单继承下的重写多态
那我们再这样:
Base b; Derived d;
Base *p = &b;
p->show();
p = &d;
p->show();
这时,结果就对了:
Base
Derived
p是一个指向基类对象的指针,第一次它指向一个Base对象,p->show()调用了Base类的show函数;而第二次它指向了一个Derived对象,p->show()调用了Derived类的show函数。
总结:也就是说,只有是指针或者引用才是真正的多态,将子对象赋给父类对象其实类型向上转型……
个人觉得C++容易弄混淆的地方(持续更新):
1.const和指针的修饰问题
const char * a; //一个指针a指向const char
char const *a; //这两个是a指向的内容是常量,不能改变
char * const a; //首先a 是指针然后还是const
const (char*) a; //这两个是a指针本身是常量,指针本身不能改变
其实,可以看出如果const修饰的char(也就是类型本身或者是 *variable对指针的解引用)就是指针指向的内容是常量,反之就是修饰指针本身的。那我们可以总结一个识别方法就是:看const 两边(当然有的只有一边)的类型是类型(指针指向的内容)就是类型变量本身是常量(如const char * a和char const *a 的const两边是char,*a)。
当然两者都是常量就是:const char * const a;第一个const是类型常量,第二个才是指针常量。同样给出 const char &a ;const char *a;在传递参数时使用。
2.数组和指针的组合问题
char * a[M]; 这是指针数组,就是每一个元素是指针的数组,每个元素都要初始化。a[M]一看就是数组,这个数组每一个元素是char *,所以可以将char *扩展为一维数组然后a[M]就是二维数组了。其实就是M个指针。
char (*a)[N]; 这是一个指针,这个指针指向N个char元素,即指向数组的指针,其实就是一个指针。把(*a)看着一个变量,这个变量是指向N个元素的指针,所以只是一个一维数组。把char (*a)[N]看成是char b[N]就可以了。
同理,也可以用修饰的道理来区分,可以自行体会。具体二维数组的动态分配的更多精彩可以查阅我的另一个博客http://dsqiu.iteye.com/blog/1683142
3.C++变量的初始化
对于内置类型局部变量不进行初始化,但是分配地址,全局变量会进行默认初始化。对于类类型局部变量(没有显式初始化)会进行默认初始化(有默认构造函数,否则报错),但其内部的内置数据成员不会进行初始化(如果在默认构造函数没有进行初始化)。数组也是同样。