title: “ 《深度探索C++对象模型》笔记(4)\t\t”
tags:

  • 笔记
    url: 1443.html
    id: 1443
    categories:
  • C/C++
    date: 2018-12-27 18:35:50

主要讲如何找到成员函数。对于程序员写得脚本如何翻译,对于对象.方式以及指针->方式的区分。原则上为了保证效率一切能转换为直接调用(全局域方法)的方法就转换,不能的那就是涉及到virtual的方法了。实质上真正被留到class中的只有virtual相关的内容,其他的都会被重命名以后作为全局域的唯一方法/数据,这样尽可能保证了非virtual相关的存取操作/函数调用与C效率一致。

相比于第一章更细化的讲解了vtbl的内容,看到这才想起来了。。还有纯虚函数需要占位的问题

笔记打乱了原书的章节顺序,整合为针对不同类型函数的说明

第四章 Function语意学

非静态成员函数

准则:非静态成员函数至少和一般的非成员函数有相同的效率。 实现方法,编译器将成员函数转换为非成员函数(全局域),步骤

  1. 改写函数signature(原型),安插额外参数,提供一个存取管道(this指针)
  2. 将函数内对非静态数据成员的存取操作改为this指针存取
  3. 将函数名mangling处理,改为独一无二的名称。
  4. 外部调用区域随之更改。

mangling:一般会将函数名增加上class名,考虑到重载还会增加参数链表,同时考虑到派生过程数据成员可能由重名,也会对数据成员增加class名,通过重命名保证所有名称为唯一的名字。当前无统一处理标准

静态成员函数

对于Point3d类的静态函数object_count();可通过((Point3D *)0)->object_count();方式调用,而避免对class的object的构建,为简化操作c++引入了静态成员函数概念,主要特点是没有this指针:

  1. 不能够直接存取class中的nonstatic members;
  2. 不能被声明为const, volatile,virtual;
  3. 不需要经由class object才被调用,可以直接经由classname::调用,当然也可以用object调用

静态成员函数一般被提取到class外,转化为一般的nonmember函数调用时,不会添加this指针,只会应用”name mangling”以及”NRV”优化. 如果取一个静态成员函数的地址,获取的是其在内存中的位置,也就是其地址,地址的类型是”nonmember函数指针”而非”class member function指针”,是int()()而不是int(class_name::)()类型。因此可以作为callback函数。

虚拟成员函数

正常在class外调用一个虚函数ptr->xx(),会转换为(*ptr->vptr[n])(ptr);通过虚函数表来获取具体的虚函数指向. 对于虚函数用inline效率会更高。在class范围内进行虚函数操作可通过class::function_name或bass_class::function_name方式调用,显示的指定具体指向以避免上述转换的影响。 C++中多态指,以一个public base class的指针或引用(引用也是指针)寻址出一个derived class object,在编译期增加以下信息实现在执行期对多态的处理,下面的信息会被施加到每一个object中,编译期可确定object的具体格式。

  1. 一个字符串或者数字来表示class的类型(真实类型)。
  2. 一个指针(vptr)指向持有程序的虚函数的执行期地址的表格virtual table。

同时,每一个object有一个vptr指向vtbl,同时每个虚函数会被记录在vtbl中并有确定的索引。每一个class有固定的vtbl。vtbl中每一项都对应class的object的active virtual functions实例地址,active virtual functions实例包括一下几个可能:

  1. 这个class本身定义的函数实例,重写(overriding)了base class virtual functions;//这是进行了继承并重写的
  2. 继承自base class的函数实例;当派生类不该写虚函数时会有这个//这是继承了并没有重写的
  3. pure_virtual_called()纯虚函数实例。可以当做 纯虚函数的空间保卫者角色,也可以当做执行期异常处理函数。//这个是纯虚函数,每个纯虚函数都会有一个pure_virtual_called()进行占位,注意是每个。。。后续继承了会将对应位置的pure_virtual_called改写为实际的实现实例。

上面三种实现,只会将对应的指针存到vtbl中。vtbl的第一项([0])是type_info,存类的类型字符串/数字 单一继承过程中,子类的vtbl前部和父类一一对应(重写了指针会变,但指向的是函数类型一样)后部是新增的虚函数。

多重继承

多重继承不像单一继承可以不断地copy基类的表然后指针改改,再加上自己的新虚函数这样操作了…… 对于第一个基类,可以完全参照单一继承,对于后续基类要特别处理。(记着前面章节说到了,有的编译器会对基类做优化,如果第一个基类没有虚函数,会把第一个有虚函数的基类放到最前面,) 在多重继承时,派生类object指针类型改为第二个base时,需要对指针做调整,以指向第二个base subobject,此时当delete时需要先将指向调到完整的object,上述都会涉及到一个offset问题,这个offset值必须在执行期调整。 内存布局来说:首先Base1,然后Base2……最后独有的非静态数据成员。对base1、2各自的虚表进行指针修改,将派生类重写过的虚函数实例的指针填入(被重写,如果1,2有同名的虚函数,无论改为任何一个base class类型指针进行调用都会调用到派生类对应的实现上)。对base1的虚表(其实也是派生类的虚表)进行扩充,增加派生类的独有虚函数以及后续所有基类(base2 base3……)特有的虚函数,也就是在base1的虚表中/派生类的虚表中完整记录了所有多重继承后的虚函数,并将指针指向真实位置。这样在派生类指针类型下调用第二个基类的独有虚函数也可直接找到。 Thunk策略:以适当的offset值调整this指针,跳转到虚函数,用于将派生类指针改为基类类型时使用(第>1个基类的类型)。微软用的address opints策略。

实际上,如果一直使用派生类的指针操作派生类的object,只用了一个虚表,并不会访问base2的虚表,后续的虚表主要是在多态过程更换了指针指向时被使用。 虚函数尽量短小,平均大小8行

虚拟继承

公共基类(虚拟继承出来的)只有一个subobject,那也就只有一个vtbl和一个vptr指针。 本书没有详细探讨这个。。。。给了建议:不要在virtual base class中声明nonstatic data member。因为当一个虚拟基类从另一个虚拟基类派生而来且两者都有虚函数、非静态成员时,会很复杂。 给了一个示意图,当唯一基类为虚拟继承来的,那么内存存储会改变:先存非静态数据+当前class的vptr(算vptr1吧)+虚拟基类的subobject(这个subobject保持了对应class的object的完整性,具有自己的vptr2)。 其中vptr2,也就是虚拟基类的虚表指针指向它自己的虚表,虚表内容不变。vptr1指向派生类的虚表,虚表的第一项写得是虚基类的偏移值,第二项才是type_info内容,等于把常规的表格整体后错了一位给offset让位,那要是有过个复杂的虚拟继承关系?是不是要让位很多很多。。。

疑问:派生类虚表第一项只写虚基类偏移值?这样如何确定派生类指针的到底是什么类型?等于没了派生类的type_info信息,虽然这个信息后置到了第二项,但是每次读取还要先判断一下第一项是不是type_info然后再决定?为什么type_info不放到第一项,然后连续放上多个offset,再往后放正常的虚函数指针?肯定是有地方存了虚拟继承的offset数量吧,把type_info放在第一位保证所有虚表一致性这样会产生什么什么问题? 第三章说的是: 涉及到虚拟继承会有两个区域,不变区域和共享区域,现正常存储不变区域内容,然后将虚拟继承导致的共享的(只有唯一实例的)subobject放在不变区域后面。上面符合这个顺序,这没错。然后不同公司给了不同的找到这个共享subobject的方法,还好上一个笔记记下来了: 微软:每个object当有虚拟继承时增加一个指针,指向增加虚拟基类表,虚拟机类表存储每个虚拟基类的指针,解决第一个问题 Sun:把虚拟基类的offset值存到vtbl虚函数表中。相应的vtbl的索引区分正负数,整数索引为虚函数,负数索引为虚拟基类offset。也解决了第一个问题,保证object大小一致。 看来这张的这个例子用的就是sun的,存了个offset到虚函数表。

其他

nonmember、static member、nonstatic member函数都是转换为一样的形式,效能一样,都等效于nonmember也就是C的函数 对于inline成员,优化后与非优化有几大差距,编译器将inline视为“不变的表达式”,在循环体中调用inline函数,如果参数无变化(包括内容也没变),编译器会视为完全无变化,直接将inline提取到循环外只运行一次计算。 nonmember、static member、nonstatic member的函数调用效能一样,在单一继承情况下,继承层次的增加有可能会影响object构造时间变化(符合需要默认构造条件时,编译器会在构造函数中或直接创建默认构造函数以调用基类的构造函数,并修改vtbl中具体指向的函数实例指针,多层次的继承后构造会增加时间) 取一个非静态数据成员的地址,得到的是该成员在class中的(offset+1)。 取非静态、非虚成员函数得到直接就是函数实例的地址,但也是不完全的,需要绑定到对应class object才可调用到member。对于double (Point:: pmf)();指向成员函数的指针。定义方法double (Point:: name)() = &Point::function_name;具体调用:Point a,b;(a.name)();(b->*name)();

这个的不完全,既然已经给了实际地址了还需要绑定class的object。我感觉首先要确定这个实例中的this具体是谁,所以必须和object绑定。就是不知道是否还需要通过绑定确定这个函数实例的具体大小

指向虚函数成员指针,获取到的是个索引值,就是这个虚函数在vtbl中的索引

class Point {
public:
virtual ~Point();
float x();
float y();
virtua float z();
}
&Point::~Point 是1
&Point::x/y 内存中地址
&Point::z 是2
float (Point::pmf)() = &Point::z;
Point
ptr = new Point;
(ptr->pmf)();
使用时可被翻译成:(
ptr->vptr[(int)pmf])(ptr) 最后是为了传object的this指针,这个被当做参数传

对于指向多重继承下的成员函数的指针。。。指针实际是个结构体。然后对结构体内容判断,通过里面的index/offset以及指针等等元素确定具体的函数实例的地址。。。嗯没认真看。微软针对这一块也有自己的处理方法。

inline

这是一个请求,不一定会被编译器接受执行。 如果发生inline: 对于形式参数,会直接被实际参数替换,如果是变量保留,如果实际参数是常量可直接计算出值,如果实际参数是函数会假如临时变量接收函数返回值并将返回值替换形势参数。 局部变量:会被mangling,避免重名。