深度探索C++对象之四 --- Function语意学

深度探索C++对象模型 — Function语意学

C++支持三种类型的member functions: static、nonstatic和virtual。下面我们就来介绍下这三种member functions的调用方式。

nonstatic member function

nonstatic member function和nonmember function有相同的效率,其实编译器内部也是把member function转化为nomember function的。
1. 首先修改函数原型,通过安插一个this指针,使class objec可以调用该函数
2. 然后通过该this指针来存取data member
3. 将member function重新写成一个外部函数,对函数名称进行mangling处理,之后在调用函数的地方也需要进行适当的修改

Name Mangling

编译器为了能够保证data member或者member function都具有独一无二的命名,对于每一个member都会做名称的特殊处理(Name Mangling),比如

  1. 对于data member,通过加上类名

    class Bar {public: int ival;}
    class Foo:public Bar {public: int ival;};
    
    那么在内部就会改写成
    class Foo {
    public:
    int ival_3Bar;
    int ival_3Foo;
    }

    通过这种方式可以绝对清楚的指出到底处理的是哪一个ival。而funciton是有重载机制的,那么就更加需要name mangling方法。

  2. 对于重载化的函数我们可以通过加入它的参数链表来进行名称处理

class Point {
public:
    void x_5Point(float newX);
    float x_5Point();
}

在内部会改写成

class Point {
public:
    void x_5PointFf(float newX);
    float x_5PointFv();
}

从上面可以看出通过将函数的参数也添加到名称处理里面去,形成独一无二的函数名称,能够解决因为函数重载导致的函数重名。另外对于不正确的调用,那么在链接期间就会无法决议而失败,这也就是“确保类型安全的链接行为”,不过这种行为只能捕捉函数标记,如果返回类型声明错误,那么是无法捕捉到的。

void print(const &Point3d) {}

这样子声明和调用就会链接出错
void print(const Point3d) {}

但是如果这样子声明和调用是检测不到的 
int print(const &Point3d) {}

这也就是为什么函数重载无法通过区分函数返回值就行判断的。

static member function

static member function的主要特性就是它没有this指针,以下的次要特性都是根源于主要特性:

  1. 首先static member function不能声明为const、volatile和virtual类型的
  2. 而且static member function独立于class object存在,因此不能读写nonstatic data member。
  3. 如果取一个static member function的地址的话,得到的就是其在内存中的地址,而不是一个指向“class member function”的指针。
&Point::count()

得到的就是
unsigned int(*)()

而不是
unsigned int(Point::*)()

很久之前c++还没有引入static member function的时候,对于这种不存取nonstatic data member的函数都是如下实现:

((Point3d*) 0)->count();

通过类型转换成一个虚拟的class object,然后再调用count函数。
现在引入了static member function则是转换成直接调用:

Point3d::count()

static member fuction因为它的特性带来了一个意想不到的好处,即static member function可以成为一个callback函数

virtual member function

如果是调用一个virtual member function的话,

ptr->normalize()

那么内部会被转化成为
(*ptr->vptr[1])(ptr)

我们之前提过,如果class含有虚函数的话,对于每个class object编译器都会产生一个vptr指向class的vtable,然后通过索引值来找到调用的函数,这里要注意后面括号里的ptr参数传入,这个即表示this指针的,表示实际要操作的class object。

另外通过对象来调用virtual function的原理并不相同

Point3d obj;
obj.normalize()

这里通过class object来调用virtual function,只可能调用Point3d::normalize(), 那么这类就不需要再使用vptr进行函数的确认,低效率!。直接调用实体的即可。因此“经由一个class object调用一个virtual function”,这种操作应该总是被编译器像对待一般的nonstatic member function一样加以决议。另外这样子做的话,那么virtual function的inline函数实体可以被扩展开来,提供极大的效率。

为了支持多态的virtual function机制,在执行期间我们需要有“执行期类型判断”,以此来确定point、reference指向的对象的实际类型,然后找到调用的函数的适当实体。那么这些目标是如何实现的呢?

首先对于什么样子的class 需要保存一些类型信息给执行期使用。通过前面的学习我们可以知道如果一个class含有virtual function就需要这份额外的执行期信息。那么什么样子的额外信息需要我们保存呢?

在执行期间,为了能够确定virtual function,我们需要知道对象的真实类型,其次我们还需要知道函数的实体位置。

单一继承

为了解决上面的问题,我们在virtual table第一个slot插入type info,之后对于每个class object都会安插一个vptr指向virtual table。该virtual table中从slot 1开始都是登记有virtual function的实体位置。编译时期,编译器能够知道每个function的地址。在复杂的派生体系中,编译器需要为每一个类的vtable改写合适的virtual function位置,大体有如下三种

  1. 通过从base class继承而来的virtual function,derived class决定不改写该function的话就直接使用base virtual function的函数实体
  2. 如果derived class决定改写继承而来的virtual funtion,那么就会填上自己的函数实体
  3. 对于derived class新增的virtual function,编译器需要增加vtable的slot,填上新增的virtual function函数实体。

如下图所示:

单一继承中每一个class都只有一个vitual table。每一个virtual function的索引值都是确定的,那么编译器直接通过索引值改写virtual function的调用方式即可。

多重继承

而在多重继承中,每一个class object就不一定只有一个vptr了,其复杂度主要是在第二个及后继的base class身上,以及“必须在执行期调整this指针”。

在多重继承之下,derived class内涵n-1个额外的virtual table

我们可以查看如下图所示的继承关系

在上述图中,Base1 subobject 和derived object的地址是一样子的,编译器在编译期间改写Base1 vpt的virtual function实体。唯一复杂的是Base2 subobject,如果通过Base1或者derived object来使用Base2,那么编译器需要修改this指针的offset,通过增加sizeof(Base1)来更新this指针以便正确的访问到Base2 subobject,与此相同的如果使用的是Base2指针来访问Base1或者derived,那么这时候就需要减少sizeof(Base2)来修改offset。

如果是不需要计算offset的virtual function的直接就是在slot中找到函数实体,而如果是需要计算offset的,那么就通过thunk技术来确定真正的virtual funtion函数实体位置。

thunk技术即通过一小段assembly代码来跳转到指定的位置执行

虚拟继承

单一继承中derived class中也只是有一个vptr指向vtable,但是在虚拟继承中derived class有两个vptr,一个是virtual base class自己的vptr指向base class的vtable;另外一个是自己的vptr,编译时期需要改写vptr以及vtable中的virtual function函数实体。具体的布局如下图所示:

单一继承中base class和derived class object的起始地址是一样子的,但是虚拟继承中的virtual base class subobject和derived class object的地址已经不相同了,那么在使用的时候编译器就需要调整this指针的offset以指向正确的virtual base class subobject。

指向member function的指针

之前我们提到过取一个nonstatic data member的地址,得到的是该member在class 布局中的offset+1,这个offset必须要结合特定的object对象地址才有效。

而如果我们取一个nonstatic member function的地址的话,如果该函数是nonvirtual的话那么得到的是它在内存中的真实地址,而这个地址也是需要绑定到某个class object的地址上才能够调用。因为nonstatic member function中需要存取nonstatic data member,而这需要通过this指针传递。

一个member function的指针可以如下声明:

double (Point::* pmf)();

初始化的话可以:
double (Point::*coord)() = &Point::x;
或者
coord = &Point::y;

使用的话:
(origin.*coord)();
(ptr->*coord)();

static member functions的类型是函数指针,而不是“指向member function”的指针。

指向virtual member function的指针

对于member function和virtual member function分别取地址的话,得到的数值是不一样的,member function取地址得到的是在内存中的实际地址,这个绑定到特定的class object的地址就可以调用。而取virtual member function的地址的话得到是它在vtable中的索引值。这两种地址都是可以赋值给“指向member function之指针的”,那么编译器就需要有办法区分当前指针是代表的实际内存地址还是vtable中的索引值?

(((int)pmf) & ~127) ? (*pmf)(ptr):
        (*ptr->vptr[(int)pmf](ptr);

这种实现技巧必须假设继承体系中最多只有128个virtual functions

多重继承下,指向member function的指针

多重继承情况下,有另外一种实现方式,通过提供如下一个结构体:

struct _mptr {
    int delta;
    int index;
    union {
        ptrtofunc faddr;
        int v_offset;
    };
};

index和faddr分别表示带有virtual table索引值和nonvirtual member function地址(当index不指向virtual table时,设置为-1)。
delta字段表示this指针的offset值,而v_offset字段放的是virtual base class的vptr位置。

Inline function

对于inline function,设计程序的时候没有强制性的将任何函数都声明为inline类型的,编译器会计算一些操作的次数来判断当前函数是否需要inline function。

而对于inline function的形式参数,每一个形式参数都会被实际参数所取代,为了防止“会带来副作用的实际参数”,通常会有如下几种情况:

  1. 如果是一个常量表达式,那么直接用常量替换
  2. 如果是一个“带副作用的实际参数”,那么就需要引入临时性对象
  3. 如果不是上述的情况那么直接替换之

另外inline function如果有局部变量的话,为了使得名称唯一也是需要引入临时性变量的,这会使得程序的体积变大。

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

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值