深度探索C++对象模型笔记 [3] 函数语意学

本文主要参考Stanley B.Lippman所著《Inside the C++ Object Model》,侯捷译。

一、函数成员的各自调用方式

Nonstatic Member Functions(非静态成员函数):C++的设计准则之一是,nonstatic member function至少必须和一般的nonmember function有相同的效率。实际上,为此,member function 会被转化为nonmember的形式!

名称的特殊处理:一般而言,member的名称前会被加上class名称,形成独一无二的命名。比如class Bar{public:int ival;…},其中的ival在name mangling后可能变为ival_3bar.这样做的原因是,经过派生操作,可以将member绝对清楚地指出来,目前,编译器对name mangling还没有统一标准。

 

Virtual Member Functions(虚成员函数):如果normalize()是一个virtual member function,那么以下调用

*ptr->normalize()

将会被内部转换为

(*ptr->vptr[1])(ptr)

其中vptr是编译器产生的指针,指向虚函数表。事实上其名称也会被mangled,因为在一个复杂的派生系统中,可能存在多个vptrs。

而上述的1是virtual table slot的索引,关联到normalize()函数

第二个ptr表示this指针。

 

Static Member Functions(静态成员函数):如果是静态成员函数,调用操作将会转换为一般的nonmember函数调用。事实上,只有当一个或者多个nonstatic data members在member function中被直接存取时,才需要class object。Class object提供了this指针用于这种形式的函数调用。这个this指针把“在member function中存取的nonstatic class members”绑定于“object内对应的members”之上。如果没有任何一个member被直接存取,事实上就不需要this指针,这时也就没有必要通过一个class object来调用一个member function。不过C++到目前为止并不能辨识这种情况。

这么一来,在存取static data members时产生了一些不规则性。如果class的设计者把static data member声明为nonpublic(这是好习惯),那么就必须提供一个或者多个member functions来存取该member。因此,尽管可以不依靠class object来存取一个static member,但其存取函数却得绑定于一个class object。独立于class object之外得操作,在某个时候特别重要----当class得设计者希望支持“没有class object存在”的情况。

解决之道,即是由cfront2.0引入的static member functions。其特征在于并没有this指针,以下的次要特性统统根源于主特性

  1. 不能够直接存取其class中的nonstatic members
  2. 不能够被声明为const、volatile或virtual(const修饰符用于表示函数不能修改成员变量的值,该函数必须是含有this指针的类成员函数,函数调用方式为thiscall,而类中的static函数本质上是全局函数,调用规约是__cdecl或__stdcall,不能用const来修饰它
  3. 不需经由class object才被调用----虽然大部分时候它是这样被调用的!

 

二、虚成员函数

在C++中,多态表示“以一个public base class的指针或引用,寻址出一个derived class object”的意思。

多态机能主要扮演一个输送机制的角色,经由它,我们可以在程序的任何地方采取一组public derived类型。这种多态形式被称为是“消极的”,可以在编译时期完成----virtual base class的情况除外。如下:

Point *ptr;

Ptr = new Point2d;

当被指出的对象真正被使用时,多态也就变成积极的了。如下:

Ptr->z();

那么,什么信息才能让我们在执行期调用正确的实例?需要知道:

  1. ptr所指对象的真实类型,这可以选择正确的z()实例
  2. z()实例的位置,以便可正确调用之

为了实现之,第一点引入了一个字符串或数字,用于表示class的类型。

第二点则引入了虚函数表的概念,为了寻找表格,每个class object都被安插了一个由编译器内部产生的指针,指向该表格;为了找到函数地址,每一个虚函数又被指派了一个表格索引值。这些工作都由编译器完成,执行期所要做的,只是在特定的virtual table slot(记录着虚函数的地址)中激活virtual functions。

一个class只会有一张虚函数表。每一个表内含其对应之class object中所有active virtual function 函数实例的地址。这些active virtual function包括:

  1. 这一类所定义的函数实例。其会改写一个可能存在的基类虚函数函数实例。
  2. 继承自基类的函数实例。这是在derived class决定不改写virtual function时才会出现的情况
  3. 一个pure_virtual_call()函数实例,它既可以扮演pure virtual function的空间保卫角色,也可以当做执行期异常处理函数(有时候会用到)

每一个虚函数都被指派一个固定的索引值,这个索引在整个继承体系中保持与特定的虚函数的关系。

那么,回到ptr->z()的例子,到底其在编译时期是如何调用的呢?

  1. 一般而言,每次调用z()时,并不知道ptr所指向对象的真正类型。但却直到ptr可以存取到对象的virtual table
  2. 虽然不知道哪一个z()函数实例会被调用,但却知道到每一个z()函数地址都会放在slot4中

这些信息可以使得编译器将之转化为:

( *ptr->vptr[ 4 ] )( ptr );

在这一转化中,vptr表示编译器所安插的指针,指向virtual table;4表示z()被指派的slot编号。唯一一个在执行期才能知道的东西是,slot所指的到底是哪一个z()函数实例。

 

多重继承下的virtual functions:

在多重继承之下,一个derived class内含n-1个额外的virtual tables,n表示其上一层的base classes的个数(因此,单一继承将不会由额外的virtual tables)。针对每一个virtual tables,derived对象中有对应的vptr。为了调节执行期链接器的效率,一些编译器把多个virtual tables连锁为一个。指向次要表格的指针,可以由主要表格名称加上offset而来。在这样的策略下,每一个class只有一个具名的virtual table。

 

多重继承下的虚表布局如下

注:

1.子类虚函数会覆盖每一个父类的每一个同名虚函数。

2.父类中没有的虚函数而子类有(即子类新增的虚函数),填入第一个虚函数表中,且用父类指针是不能调用的。

3.父类中有的虚函数而子类没有,则不覆盖。仅子类和该父类指针能调用。

 

 

虚拟继承下的Virtual functions:

注意,尽量不要在一个虚基类中声明nonstatic data members。如果这么做,会离复杂的深渊越来越近!

虚拟继承下的虚表布局如下:

 

三、指向成员函数的指针

取一个nonstatic member function的地址,如果该函数是nonvirtual,得到的结果是它在内存中的真正地址。然而这个值也不是完全的。它需要被绑定于某个class object的地址上,才能够通过它调用函数。所有的nonstatic member functions都需要对象的地址

比如,一个指向member function的指针,其声明如下:

Double(

Point::*

pmf

)();

然后可以这样定义并初始化该指针:

Double (Point::*coord)() = &Point::x;

也可以这样指定其值:

Coord = &Point::y;

 

欲调用它,可以这么做:

(origin.*coord)();

(ptr->*coord)();

指向member function的指针声明语法,以及指向member selection运算符的指针,其作用是作为this指针的空间保留者。这也就是为什么static member function(没有this指针)的类型是”函数指针”,而不是”指向member function的指针”之缘故。

 

支持“指向Virtual Member Function”的指针:

对一个nonstatic member function取地址,得到的是该函数在内存中的地址。而对一个virtual function取地址,其地址在编译期是未知的,所能知道的仅是virtual function在其相关的virtual table中的索引值,也就是说,对一个虚成员函数取地址,所能获得的仅是一个索引值。

 

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

许多编译器在自身内部根据不同的classes提供多种指向member functions的指针形式。例如Microsoft就提供了三种风味:

  1. 一个单一继承实例(其中持有vcall thunk地址或者是函数地址)
  2. 一个多重继承实例(其中持有faddr和delta两个members)
  3. 一个虚拟继承实例(其中持有4个members)

 

 

四、内联函数

关键词inline只是一种请求。如果这个请求被接受,编译器就必须认为它可以用一个表达式合理地将这个函数扩展开来。所谓合理即指,其执行成本比一般的函数调用返回机制来的负荷低

一般而言,处理一个inline函数,有两个阶段:

  1. 分析函数定义,以决定函数的”intrinsic inline ability”(本质的inline能力)。“intrinsic”在这里意指”与编译器有关”。
  2. 真正的inline函数扩展操作是在调用的那一点上。这回带来参数的求值操作以及临时性对象的管理。

形式参数:在inline扩展期间,每一个形式参数都会被对应的实际参数取代。如果说有什么副作用,那就是不可以简单地一一封塞程序中出现的每一个形式参数,因为这会导致对于实际参数的多次求值操作。一般而言,面对“会带来副作用的实参”,通常需要引入临时性对象。换句话说,如果实参是一个常量表达式,我们可以在替换前完成其求值操作;后继的inline替换,就可以直接把常量”绑”上去,如果既不是个常量表达式,也不是个有副作用的表达式,那就直接替换之

举例,假设有如下inline函数:

Inline int

Min(int i, int j)

{

         Return i<j ?i : j;

}

下面有三个调用操作

Inline int

Bar(){

Int minval;

Int val1 = 1024;

Int val2 = 2048;

/*(1)*/Minval = min(val1,val2);

/*(2)*/Minval = min(1024,2048);

/*(3)*/Minval = min(foo,bar()+1);

Return minval;

}

标记为(1)的那一行会被扩展为:

Minval = val1 < val2 ? val1:val2;

标记为(2)的那一行直接拥抱常量:

Minval = 1024;

标记为(3)的那以后则引发参数的副作用,需要引入临时对象,以避免重复求值

Int t1;

Int t2;

Minval = (t1 = foo()),(t2 = bar() +1)

t1 < t2?  t1 : t2;

局部变量:

一般而言,inline函数中的每一个局部变量都必须放在函数调用的一个封闭区段中,拥有一个独一无二的名称。如果inline函数以单一表达式扩展多次,则每次扩展都需要自己的一组局部变量。如果inline函数以分离的多个式子被扩展多次,那么只需要一组局部变量,就可以重复使用(因为它们被放在一个封闭区段中,有自己的scope).

如下:

Inline int

Min(int i, int j)

{

Int minval = I < j ? i : j;

Return minval;

}

 

{

Int local_var;

Int minval;

//…

Minval = min(val1,val2);

}

 

其在编译器中可能是这个样子:

{

Int local_var;

Int minval;

 

Int _min_lv_minval;//将inline函数的局部变量处以mangling操作

Minval =

       ( _min_lv_minval = val1 < val2? Val1 : val2),

_min_lv_minval;

}

Inline函数中的局部变量,再加上有副作用的参数,可能会导致大量临时性对象的产生。特别是如果它以单一表达式被扩展多次的话,如下面操作:

Minval = min(val1,val2) + min(foo(),foo()+1);

Inline函数对于封装提供了一种必要的支持,可以有效存取封装于class中的nonpublic数据,它同时也是C程序中大量使用的#define的一个安全替代品---特别是如果宏中的参数有副作用的话。然而一个inline函数如果被调用太多次,会产生大量扩展码,使得程序大小暴涨。

一如前文所述,参数带有副作用,或是以一个单一表达式做多重调用,或是再inline函数中有多个局部变量,都会产生临时性对象!

 

 

 

 

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值