function语意学

Posted tianzeng

tags:

篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了function语意学相关的知识,希望对你有一定的参考价值。

  static member function不能:1.直接存取nonstatic数据;2.它不能被声明为const

一、Member的各种调用方式

1.1Nonstatic member function(非静态成员函数)的调用方式

  编译器会将member 函数实例转换为对等的”nonmember函数实例。转换步骤如下

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

float Point3d::magnitude3d() const{...}

//non-const nonstatic member的扩张过程
Point3d Point3d::magnitude(Point3d *const this)

如果是member function 是const,则变成:
//const nonstatic member的扩张过程:
 Point3d Point3d::magnitued(const Point3d *const this)  

  2. 将每一个”对nonstatic data member的存取操作”改为经由this指针来存取

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

名字的特殊处理(name mangling)

  1. 一般而言,member的名字前会被加上class 的名称,形成独一无二的命名
  2. 加上参数链表
  3. 加上参数类型

1.2Virtual Member Functions(虚拟成员函数)

  如果normalize()是一个virtual member function,那么以下的调用:

ptr->normalize();
//将会被内部转化为:
(*ptr->vptr[1])(ptr);
  1. vptr表示由编译器产生的指针,指向virtual table。它被安插在每一个“声明有(或继承自)一个或多个virtual functions”的class object中
  2. 1是virtual table slot 的索引值,关联到normalize()函数
  3. 第二个ptr表示this指针

  如果使用类对象调用虚拟函数,其解析方式和非静态成员函数一样。对于上一节中的normalize()函数。

1.3Static member Function(静态成员函数)

  static member function会被提出于class声明之外,并给予一个经过mangled的适当的名称,以对象、引用或指针调用static member function将被转换为一般的nonmember函数调用。

  1. 它不能直接存取class中的nonstatic members
  2. 它不能被声明为cosnt、volatile或virtual
  3. 它不需要经由class object才被调用--虽然大部分时候它是这样被调用的

  取一个静态成员函数的地址,获得的将是其在内存中的位置,也就是其地址。由于static member function没有this指针,所以其地址类型并不是一个“指向class member function的指针”,而是一个“nonmember函数指针“

二、Virtual Member Functions(虚拟成员函数)

  在执行期间,调用操作需要执行期间获得某些相关的信息,如果把这些信息放在ptr上,那么一个指针或一个引用持有两项信息

  1. 他所参考的对象地址
  2. 对象类型某种编码,或是某个结构(内含某些信息,用以正确决议函数地址)

  这些信息放在指针上会增加额外的空间负担和降低与C语言程序的连接兼容性,所以把这额外的信息应放在对象本身。

  为了支持virtual function机制,必须首先能够对多态对象有某种形式的“执行器类型判断法”,在C++中,多态表示“以一个public base class 指针(或reference)寻址出一个derived class object”的意思。

  1. ptr的多态机能主要扮演一个输送机制,经由它,我们可以在程序的任何地方采用一组public derived类型,这种多态被称为是消极的
  2. 当被指出的对象真正被使用时,多态也就变成积极的

  看一个class是否支持多态就看他有没有virtual function,因此它就需要这份额外的执行期信息

ptr->z();
  1. ptr所指对象的真实类型,这可使我们选择正确的z()实体。
  2. z()实体位置,以便我们能够调用它。

  在实现上,可以在每一个多态的class object身上添加两个members

  1. 一个字符串或者数字,表示class 的类型。
  2. 一个指针,指向某表格,表格中带有程序的virtual functions的执行期地址。

  表格中的virutual functions地址在编译时期就可以获得virtual function的地址,此外,这一组地址是固定不变的,执行期不可能新增或替换之,由于程序执行时,表格的大小和内容都不会发生改变,所以其构建和存取都可以由编译器完全掌握,不需要执行器的任何接入。

  1. 为了找到表格,每一个class object被安插上一个由编译器内部产生的指针,指向该表格
  2. 为了找到函数地址,每一个virutual function被指派一个表格索引值

  执行期要做的,只是在特定的记录着virutual function的地址激活virutal function

  一个class只会有一个virtual table,每一个table内涵其对应的class object中所有的active virtual functions函数实体的地址,这些active virtual function包括:

  1. 这个class 所定义的函数实体,它会改写(overriging)一个可能存在的base class virtual function函数实体
  2. 继承自base class的函数实体,这是在derived class决定不该写virtual function时才会出现的情况
  3. 一个pure_virtual_called()函数的实体

  每一个virtual function都派有一个固定的索引值,这个索引值在整个继承体系中与特定的virtual function相关联。
技术图片

  上图为单一继承情况

  1. 它可以继承base class所声明的virutal functions的函数实体,正确的说,是该函数实体的地址会被拷贝到派生类的virtual table相对应的slot中
  2. 它可以使用自己的函数实体,这表示它自己的函数实体地址必须放在对应的slot中。
  3. 它还可以加入一个新的virtual function。这时候virtual table的尺寸会增大一个slot,而新的函数实体地址也会放进该slot中。

  如果这样一个调用

ptr->z();

  那么我怎么拥有足够多的只是在编译时期设定virtual function调用呢?

  1. 一般而言,我们不知道ptr所指对象的真正类型,但是我们可以知道,经由ptr可以存取到该对象的virtual table;
  2. 虽然我们不知道哪一个z()会被调用,但我知道每一个z()函数的地址都被放在slot4中,这样我们就可以转化为:
(*ptr->vptr[4])(ptr);

2.1多继承下的虚函数

技术图片

class Base1 {
public:
    Base1();
    virtual ~Base1();
    virtual void speakClearly();
    virtual Base1 *clone() const;
protected:
    float data_Base1;
}
class Base2 {
public:
    Base2();
    virtual ~Base1();
    virtual void mumble();
    virtual Base2 *clone() const;
protected:
    float data_Base2;
}
class Derived : public Base1, public Base2 {
public:
    Derived();
    virtual ~Derived();
    virtual Derived *clone() const;
protected:
    float data_Derived;
}

  在多重继承中支持virtual function,复杂度在于第二个及后继的base classes上以及必须在执行期调整this指针。如上derived支持virtual function难度落在base2上。

Base2 *pbase2=new Derived;
//新对象的地址必须调整用以指向Base2 subobject,编译期会产生以下代码:
Derived *temp=new Derived;
Base2 *pbase2=temp?temp+sizeof(Base1):0;

//如果没有这样调整指针指向“非多态运用”会失败

//当删除pbase2时,先调用正确的virtual destructor函数,然后实行delete运算符,pbase2需要调整以指出完整的对象起点
 delete pbase2;

  this指针的调整操作必须在执行期完成,即offset的大小及把offset加到this指针上头必须让编译器在某个地方插入。

Base1 *pbase1=new Derived;
Base2 *pbase2=new Derived;

//以下操作执行相同的Derived destructor
delete pbase1;
delete pbase2;
  1. pbase1不需要调整this指针,因为Base1是最左短的base class,它指向Derived对象的起始处,virtual table slot放的是真正的destructor地址
  2. pbase2需要调整this指针,其virtual table slot需要相关的thunk地址

  Thunk技术:以适当的offset值调整this指针;跳转到虚函数。
  比如代码delete pbase2;thunk看起来如下:

this += sizeof(base1);
Derived::~Derived(this);

  而对于多重继承的派生类,一个derived class内含n-1个额外的虚表,其中n表示上一层继承的base class的个数,因此对于该继承体系中的Derived,会产生2个虚表,分别对应于Base1和Base2。

  针对每一个虚表,Derived对象中由对应的vptr,vptrs将在构造函数中设定初值。这一点可以说明构造函数一般不能是虚函数。
  Derived关联的两个虚表可能有这样的名称:

  1. vptr_Derived; //主要表格,关联到Base1
  2. vtbl_Base2_Derived; //次要表格,关联到Base2

技术图片

  当将一个derived对象地址给指定的Base1指针或derived指针,被处理virtual table是主要表格,当derived对象地址给base2指针,被处理的virtual table是次要表格,sun编译器将多个virtual table连锁为一个,指向次要表格的指针,可由主要表格的名称加一个offset获得。每个class只有个具名的virtual table。

2.2虚继承的情况

class Point2d {
public:
    Point2d(float x = 0.0, float y = 0.0);
    virtual ~Point2d();
    virtual void mumble();
    virtual float z();
protected:
    float _x, _y;
};
class Point3d : public virtual Point2d {
public:
    Point3d(float x = 0.0, float y = 0.0, float z = 0.0);
    ~Point3d();
    float z();
protected:
    float _z;
}

  当point3有唯一一个base class时,继承对象模型也不像非虚拟单一继承那么简单。

技术图片 

  不要在virtual base class中声明nonstatic data member

三、函数效能

  对于nonmember、static member、nonstatic member函数都是转换为一样的形式,所以三者的效率完全一样。inline函数不仅能节省一般函数调用所带来的额外负担,也提供了程序优化的额外机会。

四、指向Member Functions的指针

double (Point::*pmf)();//声明pmf函数指针指向Point的member function

//指定其值
pmf = &Point::y;//y是Point的成员函数
//或者直接声明是赋值
double (Point::*pmf)() = &Point::y;
  
//假设一个Point对象origin以及一个Point指针ptr,那么可以这样调用:
(origin.*pmf)();
//或者
(ptr->*pmf)();
//编译器的转化为
(pmf)(&origin);
//或者
(pmf)(ptr);

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

  使用一个“member function指针”如果并不用于virtual function、多重继承、virtual base class 等情况,并不会比使用一个“nonmember function指针”的成本更高。

 4.1指向Virtual Member Functions的指针

float (Point::*pmf)()=&Point::z;
Point *ptr=new Point;

//直接调用
ptr->z();
//pmf间接调用
(ptr->*pmf)();

  内部转换为:(*ptr->vptr[(int)pmf])(ptr);

  对于一个virtual member function,取地址,得到的是一个索引值。

  假设有如下Point声明

class Point {
public:
    virtual ~Point();
    float x();
    float y();
    virtua float z();
}

  取析构函数的地址:&Point::~Point();得到索引1;取&Point::x()得到函数在内存中的地址;取&Point::z()得到索引2

  但是“指向member function的指针”评估求值,会议内该值有两种意义而复杂化,对于nonvirtual该值是一个地址,对于virtual该值是索引值,但是如何辨别这个数值是内存地址还是索引值呢?

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

  对于x&~127=0 当x<=127,这种实现技巧必须假设继承体系中的virtual functions的个数小于128

4.2重继承下,指向Virtual Member Functions的指针

  delta表示this指针的offset值,v_offset字段放置的是一个virtual(或多重继承中的第二个或后继的)base class的vptr位置,如果vptr放在对象的起始头处,该字段就没必要。

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

五、inline function

  内联函数只是建议请求编译器实行,真正决定内联还是看编译器本身。如果请求被接受,编译器必须认为它可以用一个表达式合理地将这个函数扩展开来。通常编译器会计算assignments, function calls, virtual function calls等操作的次数的总和来决定是否内联。

一般处理inline function的两个阶段

  1. 分析函数定义,以决定函数的”intrinsic inline ability”(本质的inline能力)。如果因复杂度或因其构建问题判断为不可成为inline,他会被转为static函数,并在“被编译模块”内产生函数的定义。
  2. 真正的inline函数扩展操作是在调用的那点上,这会带来参数求值操作(evaluation)以及临时性对象管理。

形式参数(Formal Arguments)

  如果实参是一个常量表达式,可以在替换前先完成求值操作,后继的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,t2;minval = (t1 = foo()), (t2 = bar() + 1), t1 < t2 ? t1 : t2;

局部变量(Local Variables)

  如果在inline定义中加入局部变量(局部变量都必须被放在函数调用的一个封闭区段中,拥有一个独一无二的名称)

inline int min(int i, int j){
    int minval = i < j ? i : j;
    return minval;
}

  假设有操作:int minval = min(val1, val2);为了维护局部变量可能变成

int __min_lv_minval;
int minval = (__min_lv_minval = val1 < val2 ? val1 : val2), 
        __min_lv_minval);

  inline函数的每一个局部变量都必须放在函数调用的一个封闭的区段中,拥有独一无二的名称。
  另外,如果扩展多次,可能会产生很多临时变量

int minval = min(val1, val2) + min(foo(), foo() + 1);

  可能扩展为

//为局部变量产生的临时对象
int __min_lv_minval_00, __min_lv_minval_01;
//为放置副作用产生的临时变量
int t1, t2;
int minval = (__min_lv_minval_00 = val1 < val2 ? val1 : val2), 
        __min_lv_minval_00)
        +
        ((__min_lv_minval_01 = (t1 = foo()), (t2 = bar() + 1), 
        t1 < t2 ? t1 : t2), __min_lv_minval_00);

  因此如果一个inline函数参数带有副作用或者以一个单一表达式进行多重调用或者函数内部有多个局部变量,这样都会产生临时对象,产生大量的扩展码,使得程序大小暴增。

以上是关于function语意学的主要内容,如果未能解决你的问题,请参考以下文章

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

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

Data语意学

Data语意学

第 3 章 Data语意学

:构造函数语意学之Default constructor的构造操作