《深度探索C++对象模型》读书笔记

第一章关于对象
C++的模型可以有多种实现方式,例如表驱动,对象模型等,如下是对象模型的实例,其中类的静态变量和静态函数单独放在类之外,包含类的虚函数的函数指针放在一个称为virtual table的虚表中,该虚表中的第一个指针通常指向类的类型信息,用于实现RTTI(runtime type information)
第二章 The semantics of Consructors构造函数语义学
2.1 default constructor的建构操作(P39)
根据C++ standard的要求,如果一个类没有任何用户定义的constructor,编译器需要生成一个trivial default constructor就是不做任何事的缺省构造函数,但是在以下几种情况下需要合成nontrivial default constructor
"带有Default Constructor"的Member class object
编译器只有在它需要的时候才合成或者扩张构造函数。
1.如果类没有任何构造函数,但它内含一个member object,这个object是有default constructor的,那么编译器将为这个class合成一个缺省构造函数,而且这个合成操作只有在构造函数真正被调用的时候才会发生。
2.一般情况下,合成的函数是inline的,如果合成的函数很复杂,这个函数将会是noninline static的。
3.如果用户定义了构造函数,而类中又含有memeber object,那么编译器会在扩张这个构造函数,在用户代码之前,先调用object的构造函数以初始化object。在用于定义了构造函数的情况下,编译器是不会再生成default constructor,只会扩展现有的构造函数
4.如果类中有多个object,那么需要根据这些object的声明顺序合成或者扩张构造函数,以调用这些object的构造函数。

"带有Default constructor"的Base Class
如果一个类派生自另外一个类,基类如果有Default Constructor,那么派生类分两种情况:
1.派生类没有任何的constructor,那么编译器会为派生类合成一个Default Constructor,用于调用父类的构造函数
2.派生类有用户定义的constructor,那么编译器会扩充这些constructor,来调用父类的构造函数

"带有一个Virtual Function"的class
如果一个类派生自另外一个类,基类或者基类链中有一个或者多个virutal class,那么编译器需要为派生类初始化其虚表指针,如果派生类没有任何constructor,那么编译器会合成一个default constructor来做这件事情,如果有constructor,那么编译器会扩充这些constructor,来做这件事情

"带有一个virtual base class"的class
主要是针对多重继承中的虚拟继承而言的,对于这种情况,必须保证继承类对象中的virtual base class object的位置是固定的,所以在初始化的时候,需要编译器需要在继承类对象中合成出一个virtual base class指针,并初始化它,这样用户代码中通过继承类对象访问虚拟继承的虚基类的成员时候,才能够正常访问。

2.2Copy构造函数的构建操作
使用到copy构造函数的三种情况:
1.直接通过赋值操作符赋值
2.通过传递参数对象
3.通过返回对象

如果类没有提供copy构造函数,那么编译器会进行default memberwise initialization,也就是如下的过程:
对当前类中的builtin类型的成员变量,执行赋值操作
对当前类中的class object类型的成员变量,递归执行其default memberwise initialization

C++把copy构造函数分为trivial和nontrivial
如果一个类是具有bitwise copy semantics,那么它就是trivial的,编译器不需要合成copy constructor,否则就是nontrivial的,需要合成copy constructor

何为bitwise copy semantics?
如果一个类对象的成员变量都是builtin类型的,或者它有class object,但是该class object的成员变量也是builtin类型的,或者说编译器没有为它合成一个copy constructor,那么该对象称为bitwise copy semantics

需要合成copy constructor(或者说不具有bitwise copy semantics)的几种情况:(P53)
1.当一个类中有一个member object,并且这个object有一个copy constructor的时候,编译器需要合成或者扩展copy constructor来调用这个对象的copy constructor
2.当一个类继承自一个基类,并且这个基类有copy constructor,不论这个基类的copy constructor是合成的还是声明的,此时编译器都需要为继承类合成copy constructor
3.当一个类具有一个或者多个virtual function的时候,编译器需要copy constructor用来初始化对象中的vptr指针,这里需要特别需要注意,只有在不同类型之间赋值的时候,才会采用这个合成copy constructor的方式,如果是相同类型的类对象之间的赋值,可以直接bitwise copy
4.当一个类派生自一个继承链,其中有一个或者多个virutal base class的时候,编译器需要copy constructor来保证virtual base class object实例的位置唯一性,与上面的情况类似,只有在不同类型的对象之间赋值才会使用这个规则,否则可以直接使用bitwise copy

2.3程序转化语意学
明确的初始化
void foo_bar(){
     X x1(x0);
     X x2 = x0;
     X x3 = X(x0);
}
编译器转化后的伪代码为:
void foo_bar(){
     X x1;//初始化操作被剥除
     X x2;
     X x3;
     
     x1.X::X(x0);//编译器安插copy constructor
     x2.X::X(x0);
     x3.X::X(x0);
}

参数的初始化
X xx;
foo(xx);
编译器转换后的伪代码为:
X __temp0;
__temp0.X::X(xx);
foo(__temp0);
同时还要把foo的函数原型修改为:
foo(X& x0);
这只是其中一种转换方法,还有另外一种copy构建的方法

返回值的初始化:
X bar()
{
     X xx;

     return xx;
}
优化一:
void bar(X& __result)
{
     X xx;
     
     xx.X::X();

     __result.X::X(xx);

     return;
}
优化二:
void bar(X& __result)
{
     __result.X::X();

      //processing result
     
     return;
}
这种优化称为NRV(named return value)优化,需要声明X的copy constructor才能触发,关于这一点实际上是有疑惑的,因为NRV本身就是为了避免调用copy constructor的,但是这里却需要类声明copy constructor才能触发NRV优化不是很奇怪么,下面的网址给出了一些讨论
关于NRV优化的疑惑问答:
其实,说的简单点,只有在copy constructor是nontrival的时候,编译器才会认为这是一个复杂对象,需要进行这种NRV的优化,不论这个copy constructor是用户定义的,还是编译器合成的,都是为了满足这个目标。

2.4 Member Initialization list
在下列四种情况下,必须使用初始化列表,否则会编译失败:
1.初始化一个reference member
2.初始化一个const member
3.调用基类的constructor,并且它有一组参数
4.调用memeber class的constructor,并且它有一组参数

初始化列表会被编译器转换为语句插入到构造函数的explicit user code之前,并且是按照member在类中声明的顺序进行初始化,而不是按照member在初始化列表中出现的顺序

第三章 Data语意学
class X {};
class Y : public virtual X {};
class Z : public virtual X {};
class A : public Y,public Z {};
sizeof(X) = 1
sizeof(Y) = 8或者4
sizeof(Z) = 8或者4
sizeof(A) = 12或者8
因为class X中没有任何的member,编译器为了区分不同的object,所以需要一个字节用来区分
Y和Z从X虚拟继承,为了保证其派生类只有一个X实例,所以他们通过一个4字节指针指向一个X的实例,同时由于Y和Z也没有member,所以有些编译器为他们分配了一个字节用以区分不同的object,同时为了4字节对齐,其大小就成为8,但有些编译器认为指向X的实例就已经能够区分这些对象,不需要再分配额外的字节,所以其大小就是4.
A从Y和Z继承,所以A中先是有Y对象的4字节,然后是Z对象的4字节,如果编译器为它添加1字节和3字节的对齐,那么总大小就是12,否则就是8

3.1Data Member的绑定
C++中有一个member scope resolution rules,其大致含义如下:1.对member function body的分析,在整个class的声明结束之后才进行,也就是说对于inline member function中的data member的绑定实在整个class声明完成之后进行的,也就是说,我们可以在inline member function中随意的使用data member,而不用担心他们没有被声明,因为编译器此时并没有对他们进行分析,在掌握了整个类的声明之后,才会对inline member function的body进行分析
2.但是对于member function的argument list,却是会立即分析的,不会等到class声明结束,因为他们是function signature,在开始就要确定下来。
typedef int length;
class Point3d
{
     public:
          void mumble(length val) { _val = val}
          length mumble() {return _val;}
     private:
          typedef float length;
          length _val;
}
这个例子很明显,mumble中定义的length类型,在开始的时候使用了全局的typedef,在碰到类的typedef的时候,会发生编译错误
3.2 Data Member的布局
基本符合原来对于C++的理解,有一点疑问的地方是P93中关于template function的指向class member的指针,在3.6节的时候,要反过来看看-----------------------------------------------------------------

3.3Data member的存取
static data member实际上就是一个全局变量,为了区分他们,C++使用了name mangling,包括两个规则:
1.保证能够推导出独一无二的名字
2.保证能够通过推导后的名字,反向得到原来的名字
nonstatic data member的存取
origin._y = 0.0
&origin._y = &origin + (&Point3d::_y - 1)也就是说origin._y的地址等于origin对象的地址加上_y data member在对象中的偏移量,在3.6节中会有指向data member的指针------------------------------------------------------------
看如下两种表达式的区别:
Point3d origin,*pt=&origin;
origin.x = 0.0//此时可以确定origin的对象类型,从而在编译期就能知道x的偏移量
pt->x = 0.0//此时对于pt指向一个继承类,并且其继承结构中包含虚拟继承的情况,在编译期就无法确定x的偏移量,只要在运行期确定其具体的对象类型,才能知道,此时这两个表达式是有区别的。

3.4 继承与Data member
只要继承不要多态
C++保证,出现在派生类中的基类子对象有其完整原样性。也就是说,在派生类中的基类成员完全保持基类对象的结构。

加上多态
编译器需要做如下的调整:
1.引入一个virtual function table,用来存储类的虚函数地址,可能还要加入类的RTTI信息,注意virtual function table是类相关的,每个类只需要有一个,其中的信息是在编译器就能够确定的。
2.在每个类的object中加上一个指向virtual function table的指针
3.修改构造函数,在初始化的时候,让对象的virtual function table的指针指向正确的地址。
4.修改析构函数,在析构的时候,设置virtual function table的指针为空
此时,编译器考虑的一个问题是,对象中指向virtual function table的指针在对象的内存布局中的位置,在早期的cfront编译器中,为了在内存布局上与C的struct兼容,把这个指针放在了对象的最后。但是现代编译器都是把这个指针放在对象的第一个4字节。
特别需要说明的是,所谓的多态,都是在使用指针的情况下出现的,此时指针指向的对象的类型在编译期无法确定,只能等到运行期通过不同的虚函数表指针才能确定。对于在编译期直接使用对象调用虚函数的情况,由于编译器可以确定对象的类型,可以优化为直接调用,不用等到运行期,了解此点很重要,可以防止多态的滥用。

多重继承
多重继承对对象的内存结构的影响是,按照多重继承的声明顺序,在内存中依次排放base class subobject。
由此导致的问题是派生类对象与第二或者后继的基类子对象之间的转换问题
设有如下的派生关系:
class Point2d {};
class Point3d : public Point2d {};
class Vertex {};
class Vertex3d : public Point3d, public Vertex {};

Vertex3d v3d;
Vertex3d *pv3d;
Vertex *pv;
Point2d *p2d;
Point3d *p3d;

pv = &v3d;
Vertex3d是从Vertex继承的,所以可以这样赋值,但是在编译器内部将被转化为:
pv = (Vertex*) ((char*)&v3d + sizeof(Point3d))
对于如下的赋值:
pv = pv3d;
编译器转化为:
pv = (pv3d)?(Vertex*)((char*)&v3d + sizeof(Point3d)):0;

虚拟继承(P116)
虚拟继承对对象的内存布局的影响是,由于出现菱形继承,对象中出现shared virtual base class subject,在对象的内存布局中需要一个共享的虚基类子对象,这样在最下方的派生类中存取虚基类的成员的时候,能够保证唯一的存取。
目前编译器采取的方式是把virtual base class subject放在了对象的末尾,为什么要这样设计呢,如果放在对象的开始不是可以省很多事情么?原因如下:
如果对象有多个virtual base class subject,把他们放在对象的开始位置会导致对象模型异常复杂,不仅影响virtual base class subject,而且也影响了派生类subject。所以大部分编译器采用了,把不变的派生类subject放在对象开始,而把可变的virtual base class subject,放在对象末尾的方式。
由于这种设计方式,在不同的派生类中,virtual base class subject的位置变得不固定,这样导致编译期无法通过一个通用的方式处理virtual base class subject的成员变量的存取。cfront采用的方式是在派生类对象中加入一个指针,指向virtual base class subject,但是此方法有两个缺点:
1.在每个对象中都加入一个指向virtual base class subject指针,导致空间的浪费
2.在出现n多次的虚拟继承的情况,需要通过这个指针进行n的迭代,导致时间上的浪费
一种推荐的处理方法是:
在派生类的virtual function table中放置virtual base class subject的offset,如果出现n次继承的情况,在编译期就可以把这n次继承的偏移量都放在这个表中。由于virtual function table每个类只有一个,节省了空间,在n次虚拟继承的情况下,也节省了时间。由此在访问virtual base class subject的成员变量的时候,需要放过虚拟继承得到virtual base class subject的offset,这种成本也是可以接受的。

同样,对于使用一个有虚拟继承的对象,直接访问其virtual base class subject的成员变量的情况,在编译期就可以直接存取,因为此时就能确定其类型,继而知道偏移量。对于虚拟继承的情况,由于virtual base class subject被放置在对象布局的末端,导致不同的派生类中virtual base class subject的偏移量不同,所以如果采用指针存取virtual base class subject的成员变量这种情况,在编译期由于无法判断对象的类型,继而无法知道存取变量在对象中的偏移量,只能等到运行期,通过virtual function table得到virtual base class subject的offset。

3.5 对象成员的存取效率
方法:
1.从对象的内存结构,判断存取效率
2.从编译器转换后的伪代码或汇编代码,判断存取效率
结果:
1.struct的存取效率是最高的
2.使用inline函数,在优化打开的情况下,可以达到与struct相同的存取效率
3.虚拟继承的成员对象的存取效率最低

3.6指向Data Member的指针
看如下的代码:
class Point{
public:
     Point() {x=1;}
     int x;
};

class Point2D: virtual public Point{
public:
     Point2D() {y=2;}
     int y;
};

class Point3D: virtual public Point2D{
public:
     Point3D() {z=3;}
     int z;
};

int _tmain(int argc, _TCHAR* argv[])
{
     printf("&Point::x=%p\n",&Point::x);
     printf("&Point2D::y=%p\n",&Point2D::y);
     printf("&Point3D::z=%p\n",&Point3D::z);

     Point p1;
     Point2D p2;
     Point3D p3;

     printf("sizeof Point=%d\n",sizeof(p1));
     printf("sizeof Point2D=%d\n",sizeof(p2));
     printf("sizeof Point3D=%d\n",sizeof(p3));

     return 0;
}
其中&Point::x得到的是Point的成员变量x在Point对象中的偏移量,使用指针表示为int Point::*,使用Point对象的指针像如下这样使用指向成员变量的指针
Point *p;
int Point::*pMem = &Point::x;
p->*pMem = 4;
P132有一个关于多重继承情况下,使用指向成员变量的指针进行存取的例子,说明编译器在对指向成员变量的指针进行操作的时候,是要根据对象指针进行冲定义的。
梳理虚拟继承的内存结构,以上面代码为例
Point的内存结构如下:
-------------------
|         x         |
-------------------
Point2D的内存结构如下:
                                  ----------------
                                  |       8        |
-------------------  vptr    ----------------
|               ----+------->|    RTTI      |
-------------------            ----------------
|        y           |
-------------------
|        x           |
-------------------
Point3D的内存结构如下:
                                  ----------------
                                  |       12      |
                                  ----------------
                                  |       8        |
-------------------  vptr    ----------------
|               ----+------->|    RTTI      |
-------------------            ----------------
|        z           |
-------------------
|        x           |
-------------------  vptr   -----------------
|              -----+------>|     RTTI      |
-------------------          ------------------
|        y           |
-------------------

4. Function 语意学
Nonstatic member Functions的转换步骤:
1.改写函数的signature,添加this指针作为第一参数
2.将对nonstatic data member的操作,改为经过this指针的操作
3.将member function改写为一个外部函数,对函数名称进行mangling,是它在程序中的名字独一无二。

name mangline
data member name mangling
[member_name]_[class_name]
member function name mangline
[member_name]_[class_name]_[parameter_type_list]

4.2 Virtual Function Members
考虑这样一个问题,编译器在遇到ptr->normalize()调用的时候,会如何转化该调用?按照目前的理解应该是这样的:
1.编译器应该在编译期间维护了各种类型信息,一来自己用,二来对于有虚函数的对象,RTTI要加入到virtual table中。
2.编译器是知道ptr指针的类型,如果这个类型有virtual function,那么其内存结构中就会包含vptr,否则就是与C结构兼容的内存结构。
3.根据这些信息,如果normalize是virtual function,那么它将会被转化为(ptr->vptr[n])(ptr)
4.如果normalize不是virtual function,那么它将会被按照member function的规则转化为普通的函数调用,在链接期如果没有相应的函数与之链接,则会报错。

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值