C++对象模型——Function语意学

文章目录

  • 前言
  • 一、Member的各种调用方式
  • 二、虚拟成员函数
  • 三、函数的效能
  • 四、指向Member Function的指针


前言

本章主要介绍了各种成员函数的调用方式,特别是存在继承和多态时的虚函数调用。


成员函数可以被看作是类作用域的全局函数, 不在对象分配的空间里,而存在于代码段, 只有虚函数才会在类对象里有一个指针, 存放虚函数的地址等相关信息(即虚函数表)。调用成员函数时,类对象的地址通过this指针作为参数隐式传递给成员函数,成员函数通过对象地址隐式访问成员变量。this指针是由编译器生成,当类的非静态成员函数的参数个数一定时,this指针存储在ECX寄存器中;若该函数参数个数未定(可变参数函数),则存放在栈中。

一、Member的各种调用方式

1、C++的设计准则之一就是:nonstatic member function 至少必须和一般的 nonmember function 有相同的效率。乍看之下,似乎nonmember function比较没效率,但实际上,nonstatic member function会被编译器进行如下的转换,内化成nonmember的形式:

Type1 X::foo(Type2 arg1) { … }
//会被转换为如下的普通函数:
void foo(X *const this, Type1 &__result, Type2 arg1) { … }

总结起来,转化步骤为:

(1)改写函数的signature(函数原型)以安插一个额外的参数(this指针)到member function中,用以提供一个存取管道,使class object得以将此函数调用;

(2)将每一个“对nonstatic data member的存取操作”改为由this指针来存取;

(3)将member function重新写成一个外部函数,将名称经过“mangling”处理,使它成为程序中独一无二的词汇。

2、name mangling会将参数和函数名称编码在一起形成独一无二的标记,但是如果声明extern “C”,就会压抑nonmember functions的“mangling”,因为C语言不支持函数重载,所以它的name mangling比C++简单得多,extern "C"明确告诉编译器要使用C语言的规则。

这种策略使得编译器在不同的被编译模块之间达成了一种有限形式的类型检验。任何不正确的调用操作在链接时期就因无法决议而失败,但是它只能捕捉函数的标记错误,而不能捕捉“返回类型”声明错误。

3、如果两个成员函数均是virtual function,并且其中一个在另一个中被调用,则被调用的那个成员函数最好是显式调用,并且声明为inline函数。因为此时,this指针的类型已经明确了,不需要再通过虚拟机制来调用被调函数,显式调用可以压制因虚拟机制而产生的重复调用操作。声明为inline,可以让vitrual function的决议方式和nonstatic member function一样,并且编译器会将内联函数的调用表达式用函数体替换,而不需要等到运行时再调用,这大大提高了效率。

4、静态成员函数其实就是带有类scope的普通函数,它没有this指针,所以它的地址类型并不是一个指向成员的指针而仅仅是一个普通的函数指针而已。
静态成员函数是作为一个callback的理想对象,在类的scope内,又是普通的函数指针。

5、因为静态成员函数没有this指针,所以:

(1)它不能够直接存取其class中的nonstatic members;

(2)它不能够被声明为const、volatile或virtual;

(3)它不需要经由对象调用。

二、虚拟成员函数

1、为了支持virtual function机制,我们需要一些执行期信息,需要考虑两个问题。

(1)哪个对象需要这些信息呢?

对于呈现多态特性的class需要额外的执行期信息。识别一个class是否支持多态,唯一的适当方法就是看它是否有任何的virtual function。只要class声明有任意一个virtual function,那么它就需要额外的执行期信息vtable。

(2)什么样的额外信息需要存储起来呢?

  • 指针所指对象的真是类型,用以选择正确的函数实例;
  • 函数实例的位置,以便能够调用。

2、单一继承时的内存布局

这种内存布局下,virtual function是如何工作的呢?因为在调用一个成员函数ptr->z()时:
(1):虽然不能确定ptr直接指向的类型,但是可以经由ptr找到它的vtable,而vtable记录了所指对象的真正类型(一般对象的type_info放在vtable的第1个slot里);
(2):虽然不能确定应该调用的z函数的真正地址,但是可以知道它在vtable中被放在哪一个slot,于是就直接去vtable中相应的slot中取出真正的函数地址加以调用。

内存布局如下:
C++对象模型——Function语意学_第1张图片
3、多重继承下的内存布局

在多重继承中比单一继承更复杂的地方在于对非第 1 基类的指针和引用进行操作时,必须进行一些执行期的调整 this 指针的操作。比如对于简单的delete操作: deleta base2;由于base2可能没有指向对象的起始地址,这样简单的删除操作会引发灾难,所以需要对base2做执行期的调整才能正确的delete对象。
在多重继承下,一个Derived Class可能同时含有多个vptrs指针,这取决于它的所有基类的情况(想想基类完整性)。也可能有对应的多个vtables(如cfront),但也可能无论如何只有一个vtable(把所有的vtables合成一个,并使得所有的vptrs都指向这一个合成的vtable +offset,如Sun的编译器),这都取决于编译器的策略。

内存布局如下:
C++对象模型——Function语意学_第2张图片
4、虚拟继承下的内存布局
C++对象模型——Function语意学_第3张图片
Lippman建议:不要在一个virtual base class中声明nonstatic data members。如果一
定要这么做,那么你会距离复杂的深渊愈来愈近,终不可拔。

三、函数的效能

1、这里的 函数性能测试表明, inline 函数的性能如此之高,比其它类型的函数高的不是一个等级。因为inline函数不只能够节省一般函数调用所带来的额外负担,也给编译器提供了程序优化的额外机会。

四、指向Member Function的指针

1、取一个nonstatic member function的地址,如果该函数是nonvirtual,则得到的结果是它在内存中的真正地址。然而这个值是不完全的,它需要被绑定于某个class object的地址上,才能够通过它调用该函数。
很明显,这个nonstatic member function被编译器添加了一个参数this,如果不绑定于
class object就无法传递这个this指针。

(origin.*fptr)(); 
//会被转换为 
(fptr)(&origin);

指向member function的指针的声明语法,以及指向“member selection运算符”的指针,其作用是作为this指针的空间保留者。

2、对一个 virtual member function 取其地址,所能获得的只是一个 vtable 中的索引值。

3、为了让指向member functions的指针也能够支持多重继承和虚拟继承,设计了如下的结构体:

struct __mptr{
    int delta; //this指针的offset值
    int index; //virtual table索引,当index不指向virtual table时会被设为-1,每一个调用操作都需要检查是virtual还是nonvirtual
    union{
        ptrtofunc faddr; //nonvirtual member function地址
        int v_offset; //virtua(或多重继承中的第二或后继的)base class的vptr位置
    }
}

微软拿掉了上述的检查操作,而是引入了一个vcall thunk。faddr不是指向nonvirtual member function地址就是指向vcall thunk的地址,vcall thunk会选出并调用相关的virtual table中的适当slot。

4、处理一个inline函数,有两个阶段:

(1)分析函数定义,以决定函数的“intrinsic inline ability”,如果函数因其太复杂或因其建构问题,被判断不可inline,则它会被转化为一个static函数。

(2)真正的inline函数扩展操作是在调用的那一点上。这会带来参数的求值操作以及临时对象的管理。

5、形式参数

(1)如果实际参数是一个常量表达式,则先求值,再替换;

(2)如果实际参数不是一个常量表达式,也不是带有副作用的表达式,就直接替换;

(3)如果实际参数“会带来副作用”,则引入临时变量来避免重复求值。

对于函数: inline int min(int i, int j) { return i < j : i : j; }
如果这样调用:minval = min(foo(), bar() + 1);
编译器就会自动的引入临时变量并赋值t1=foo(), t2=bar(),避免了对这个函数的多次调用。

因此,知道编译器会自动的做这些优化,就没有必须自己去画蛇添足的手动引入临时变量了。

6、一般而言,inline函数中的每一个局部变量都必须放在函数调用的一个封闭区段中,拥有独一无二的名称。

7、inline中再调用inline函数,可能使得表面上一个看起来很平凡的inline却因连锁的复杂性而没有办法扩展开来。
对于既要安全又要效率的程序,inline函数提供了一个强而有力的工具,然后与non-inline函数比起来,它们需要更加小心的处理。

你可能感兴趣的:(c++,c++)