读书笔记《深度探索c++对象模型》(4) - 类函数语意

和自甴很熟 提交于 2019-12-01 05:00:21

 

一、类成员函数调用方式

  1. 非静态成员函数,要求非静态成员函数应与一般的非成员函数有相同执行效率,本质上,非静态的成员函数会被转为对等的非成员函数的实体(即:非静态成员函数会被改写,加入this指针,并对函数内部的非静态成员变量的存取修改为由this指针来存取,最后将该函数重写为一个外部函数进行名称重命名,此时非静态成员函数与非成员函数别无二致了)。
  2. 在非静态成员函数被重写为外部函数时的名称重命名操作,但此操作因不同的编译器实现不同,一般基于类名和函数参数类型、个数等来实现(为区分不同类重名成员变量、重载的非静态成员函数)(重命名操作,其实除了包括成员函数和虚函数外,静态成员函数也会被重命名)。
  3. 虚成员函数,因虚函数的存在,类中需增加一个vptr的指针,当然该指针名称也会被重命名以满足区分多继承时的多个vptrs。当通过对象或指针调用虚函数时,需先获取到虚指针再加上对一个虚函数所在的slot索引数字即可,即形如:(*ptr->vptr[someindex])(ptr)。
  4. 虚成员函数在通过指针对象调用虚函数和通过类对象调用虚函数的区别,前者可能需要通过(*ptr->vptr[someindex])(ptr)方式来调用,而后者可能直接用重名后的虚函数直接调用即可如:somefunc_XXX(&someobj)而不是(*obj.vptrsomeindex[(&obj))的方式;故而对象调用虚函数更为直接。
  5. 静态成员函数,无论通过类对象调用或指针对象或者直接由类调用,均会转为直接对重名后的函数调用;此外静态成员函数意味着:1) 不能直接存取类中的非静态成员含成员函数 2) 不能被声明未constvolatilevirtual3) 可不需要通过对象或对象指针来访问。
  6. 静态成员函数也会被重命名,另外通过取地址获取到的地址类型为非成员的函数地址而不是类成员函数地址,因缺少this指针,基本上可以认为非静态成员函数和非成员函数一致了。鉴于没有this指针参数,静态成员函数常常可以作为回调函数、线程运行函数等。

二、虚成员函数

  虚函数的调用,当由一个指向子类的基类指针对象或引用对象时,需要在运行期获取到足够的信息,以表明当前实际执行实体。在类对象模型中,只要有虚函数,则就需要执行期的额外信息(当前指针指向的真实类型以及实际的实体所在位置地址)。这就涉及到vptrvtable表的构建。

  1. 虚函数的实体地址会被映射到对应虚表所在的slot中,另外纯虚函数也会占用一个slot尽管没有函数实现体。
  2. 子类中的虚表对应拷贝继承于基类,但是若子类重写了某虚函数实现则替换对应slot的函数实现体地址即可,若新增了虚函数,则在虚表中后面增加slot即可,保证父类的虚表中slot索引与子类的一一对应。单一继承对象模型布局如下图所示:

   3. 多重继承中,多重继承可能产生多个虚表,虚函数调用复杂度主要体现在第二个或后继的其他基类(当转为基类指针对象时可能需要调整地址,以指向对应的基类对象部分,另外就是由基类指针对象调用子类的虚函数也需要调整指向位置),另外就是在执行期需调整所谓的this指针的问题。多重继承对象模型布局如下图所示:

  4. 虚继承体系下的虚函数调用,相对于非虚的单一继承,没有那么自然,在基类和子类的转换时均要调整this指针以及计算偏移量以得到实际的类型对象部分的地址,虚继承对象模型布局可能如下图所示:

  5. 类似于成员的存取效率,函数的调用执行效率,在非静态成员函数(与静态成员函数、普通函数执行效率一样的均较高,因均会转化为完全相同的外部调用形式)、虚成员函数、多重继承、虚拟继承中均会有所偏差,效率将逐步降低,因涉及到构造函数调用、虚表构建、虚函数调用的this指针调整以及对象的成员变量存取效率等影响。

三、指向成员函数的指针

  1. 取非静态数据成员的地址,将得到该成员变量所在类的内存布局中的位置,但其也需要绑定于某个类对象的地址上才可以被存取;同样取非静态的成员函数地址,也需要绑定于某个类对象的地址上才能够通过它调用该函数,因非静态成员函数至少需一个对象的地址即参数this指针。对于一个成员函数指针,若不用于虚函数、多重继承或虚基类等情况时此与非成员函数指针的成本差不多,编译器可提高相同的执行效率,若是用于虚函数、多重继承以及虚函数的类时,可能会有所不同。
  2. 指向虚成员函数的指针与指向一般成员函数的指针的区别,前者被编译器转化获取到一个该虚函数在虚表中的索引,而后者则为实际函数在内存中的地址,然而对于调用成员函数指针和调用指向虚成员函数的指针,编译器需能识别出来并做区分,因为这两种都可以由一个类对象的方式来调用它们,如:假设pmf为指向成员函数的指针(假设为无参数的成员函数), (*ptr->vptr[(int)pmf])(ptr)与(ptr->*pmf)()。
  3. 指向成员函数的指针所取得的值,依照不同编译器实现不同,一般分为多种情况,以微软的VC编译器为例:

    1) 单一继承时,若该指针为指向虚成员函数时为一个vcall thunk地址;若该指针为指向普通成员函数地址,则为实际的函数所在地址;

    2) 多重继承和虚拟继承时,为其他的一个结构(该结构以区分普通成员函数还是虚函数以及对应的函数地址或虚函数表索引值及this指针调整值之类的),以支持并区分此两类取成员函数地址的情况。

 

四、inline内联函数

 

  编译器会对inline内联扩展与否进行计算判断,最终是否被内联需要得到一个测试,以计算权重总和。不同的编译器计算方式或测试方法不同,是否被内联扩展只能通过汇编代码来查看。

  Inline函数对于封装提供必要的支持(宏中无法调用类中私有变量或被保护的变量,而内联函数可以直接存取),另外相对C#define宏来说更为安全(宏的参数传入被扩展时可能是非预期的(一般最好用()包含传入的参数或者宏声明里用()包含形参),宏也仅仅是文本替换);此外若inline函数若被调用太多次数也会产生大量的扩展代码,进而使得程序的大小膨胀,一方面因为inline函数的参数可能带来副作用,引入一些局部变量、临时变量等,单一表达式多重调用也会产生临时对象,编译器也不一定会移除它们(内联扩展发生在编译期,宏替换发生在预处理期)。

  此外若inline函数中若调用了其他函数或inline函数可能因复杂度连锁反应,导致无法内联扩展;故而inline函数可以使得程序运行效率更高,但可能也会使得程序膨胀,所以需要谨慎处理;故而一般情况下,若是确定可内联或需要被内联,则可用inline修饰;否则可交由编译器来自行判断处理,是否会被内联扩展。

 

易学教程内所有资源均来自网络或用户发布的内容,如有违反法律规定的内容欢迎反馈
该文章没有解决你所遇到的问题?点击提问,说说你的问题,让更多的人一起探讨吧!