读书笔记《深度探索c++对象模型》(3) - 类成员变量的数据语意
一、类数据成员绑定 1.一个空类的大小不会为空,一般为一个char,即1个字节大小,其为了区分不同两个空类对象,需要一个地址来表示。 2.一个类的sizeof的大小,由多个方面影响: 1)语言本身的负担(如虚指针) 2)不同编译器对特殊情况的优化处理(如继承于一个空类的类,编译器可能做优化) 3)alignment对齐方式的限制(因为对齐的原因,成员变量的顺序有时也会对sizeof大小影响)。 3.由typedef重声明的类型在类外前面和类中均有时,则编译器可能会按照第一次被决议的那个typedef使用,因此此处存在一个缺陷,即在未发现到类中的typedef时,均使用外层的,导致类型不是预期的类中的那个类型,故而一般情况下,类中的typedef应该放在类中的起始位置处最为安全,而不是声明成员变量处;以此来解决数据成员类型绑定的问题。 二、类数据成员布局 1.非静态的数据成员在类对象中的排列顺序与其声明顺序一致,虽然中间可能会插入一些对齐需要的字节,另外一个控制访问段(public,private,protected)中的数据成员顺序一致,但不同的控制访问段的数据成员间的顺序不一定与类中声明的顺序一致。 2.类中若是有vptr,则vptr的位置可能位于类对象中的前端也可能在最后段,甚至也有可能在数据成员的中间。不过一般为最前或最后。 3.类中的控制访问段的多少没有关系,不影响总的字节占用大小。 三、类数据成员的存取 静态数据成员变量: 1.每个静态成员变量仅有一个实体,存放在程序的数据段中,可被认为是一个全局变量,只是其仅在该类的生命范围内可见。 2.静态成员变量的存取,无论通过类或类对象来访问,两者均一样的,没有开销方面的影响,无论该静态成员变量位于当前类或基类或是虚继承来的,因为仅有一个实体,存取操作是直接无差别的。 3.对静态成员变量取地址时,因为该变量并不在类对象中,故取得的仅仅是外部的指向其数据类型的指针而已。 4.不同类若生命相同名称的静态成员变量时,数据段中的同名成员变量会被编译器重名为其他可识别为所属不同类的变量。 非静态数据成员变量: 1.每个非静态数据成员均存在于一个类对象实例中,不能被直接存取,在类成员函数中存取时也是通过this指针来获取操作的(成员函数的第一个参数为隐藏的指向当前类的this指针对象)。 2.存取一个非静态的数据成员,是通过指向该类对象的指针加上该成员的偏移量得到的,该操作为编译期就可以决定的,此与C的struct结构存取等效率差不多的。 3.基于2种的说明,若该成员变量为struct结构体的成员、类成员、单一继承、多重继承、虚拟继承等可能存取效率会有所不同。尤其是对虚基类中的成员变量(即通过对象存取与通过对象指针地址来存取有区别,后者可能会有个间接性的开销)。 四、继承与类数据成员 1.子类的数据成员与基类的数据成员在子类中的对象模型中的顺序由编译器决定的,但一般都是基类成员在前(虚基类除外,形式又不同)。 2.继承与数据成员的对象模型的几种影响关系 1)无虚函数的单继承体系时,与C struct一样的,不会增加内存占用和存取开销; 2)继承体系中,要保证子类的对象包含父类对象的模型原样性(含vptr),故而将成员变量分散在多个类继承体系和放在一个类中时,在最终的占用sizeof大小可能很不同的(因每个类占用字节大小都需要对齐操作以及按位拷贝的影响,此时为了保持子类中的各父类的原样,则也会继承到补齐的字节,产生类似于膨胀的空间占用)。 3)继承体系中,若有虚函数(非虚拟继承)时,vptr可能放置的位置在最前或最后,若是最前则可能就不满足顺其其然的其实地址,因此此时将子类地址传给父类时需要编译器调整实际的有效地址;相反,若是vptr放置在最后,则可直接处理无序调整。 4)多重继承时(非虚继承),子类对象地址可能与第一个顺序继承的父类地址一致(编译器一般按照此顺序进行对象布局的,但不同编译器处理方式可能不同),若是取其他基类地址,则需要计算出各个基类对象占用偏移;虽然如此,存取基类的成员变量是不会由开销的,因为均存在于子类对象中,编译器在编译期已计算出其位置(通过对象或引用来存取,若是通过基类指针可能会有额外开销)。 5)虚继承时(菱形继承时),因要保证父类原样性,又要保证子类中仅有一个虚基类的实体,故子类中需要特别处理;一般的处理方式为:将类分为两个部分,一个不变的局部和一个共享的局部。其中不变的局部用友固定的offset值,其主要支持直接存取;而共享局部主要支持需基类的对象部分,其位置随每次派生操作而变化,仅能支持间接存取,不同编译器有不同的实现策略。 策略1:子类对象模型中内部插入一个指向基类对象部分的地址开始位置的指针,在子类中调整各部分基类部分的指针指向,如下图所示: 策略2:子类对象模型中不插入额外的指针,而是在虚表里的最前面的slot加入一个当前类型起始位置的相对偏移量。由该偏移量来查找基类部分的实际地址,如下图所示:
策略2相对来说,类对象模型中没有增加额外的指针,更能保持基类的原样性。但不论哪种方式,在类型相互转换的过程中需要计算相对偏移,找到正确的位置地址。
五、类对象成员的效率
通过虚继承方式来存取数据,存在一定的间接性,纵使通过对象来存取成员变量时在编译期已计算出来,但其会影响到缓存器的优化能力,另外此时若通过基类指针或引用的方式,则效率也会进一步降低。这也是虚继承带来的性能损失。
六、指向类数据成员的指针
1.可用来确定一个vptr所在类对象模型中的最前端还是最后段; 2.执行基类的指针在转换类型过程中需计算偏移量得到正确的类型的指针地址; 3.执行数据成员的地址的指针调用和直接存取间的效率影响,在未优化时会比直接存取的效率低很多,此外在继承体系下(含虚继承),通过指向数据成员的指针来操作成员则会增加间接性,即取地址和offset偏移计算;另外还会影响放在缓存器的优化操作,导致效率降低。