Function语意学(The Semantics of Function)

static member functions不可能做到的两点:(1)直接存取nonstatic数据,(2)被声明为const的。

Member的各种调用方式

Nonstatic Member Functions(非静态成员函数)

C++的设计准则之一就是:nonstatic member function至少必须和一般的nonmember function有相同的效率。比如,要在下面两个函数之间作选择:

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

选择member function不应该带来什么额外负担。这是因为编译器内部已将“member函数实例”转换为对等的“nonmember函数实例”

下面是magnitude()的一个nonmember定义:

float magnitude3d(const Point3d *_this){
return sqrt( _this->_x * _this->_x +
_this->_y * _this->_y +
_this->_z * _this->_z );
}

乍见之下似乎nonmember function比较没有效率,它间接地经由参数取用坐标成员,而member function却是直接取用坐标成员,然而实际上member function被内化为nonmember的形式。 转化步骤如下:

  1. 改写函数的signature(译注:意指函数原型)以安插一个额外的参数到member function中,用以提供一个存取管道,使class object得以将此函数调用。该额外参数被称为this指针。
  2. 将每一个“对nonstatic data member的存取操作”改为经由this指针来存取。
  3. 将member function重新写成一个外部函数。将函数名称经过“mangling”处理,使它在程序中称为独一无二的词汇。

名称的特殊处理(Name Mangling)

一般而言,member的名称前面会加上class的名称,形成独一无二的命名。例如下面的声明:

class Bar{ public: int ival; ... };

其中的ival有可能变成这样:

//member经过name-mangling之后的可能结果之一
ival_3Bar

为什么编译器要这么做?请考虑如下派生操作:

class Foo : public  Bar{ public: int ival; ... }

记住,Foo对象内部结合了base class和derived class两者:

//Foo的内部描述
class Foo{
public:
int ival_3Bar;
int ival_3Foo;
...
};

不管你要处理哪一个ival,通过"name mangling",都可以绝对清楚地指出来。由于member function可以被重载化(overload),所以需要更广泛的mangling手法,以提供绝对独一无二的名称。

把参数和函数名称编码在一起,编译器是在不同的编译模块之间达成了一种有限形式的类型检验。举例如下,如果一个print函数被这样定义:

void print(const Point3d& ){ ... }

但意外地被这样声明和调用:

//以为是const Point3d&
void print(const Point3d );

两个实体如果拥有独一无二的name mangling,那么任何不正确的调用操作在链接时期就因无法决议(resolved)而失败。有时候我们可以乐观地称此为“确保类型安全的链接行为”(type-safe linkage)。我说“乐观地”是因为它可以捕捉函数标记(signature,亦即函数名称+参数数目 + 参数类型)错误;如果“返回类型”声明错误,就没有办法检查出来。

Virtual Member Functions(虚拟成员函数)

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

ptr->normalize();

将会被内部转化为:

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

其中:

  • vptr表示由编译器产生的指针,指向virtual table。它被安插在每一个"声明有(或继承自)一个或多个virtual functions"的class object中。事实上其名称也会被"mangled",因为在一个复杂的class派生体系中,可能存在有多个vptrs
  • 1 是virtual tabel slot的索引值,关联到normalize()函数
  • 第二个ptr表示this指针

Static Member Functions(静态成员函数)

如果Point3d::normalize()是一个static member function,以下两个调用操作:

obj.normalize();
ptr->normalize();

将被转换为一般的nonmember函数调用,如:

//obj.normalize()
normalize_7Point3dSFv();
//ptr->normalize()
normalize_7Point3dSFv();

在引入static member functions之前,C++ 语言要求所有的member functions都必须经由该class的object来调用。而实际上,只有当一个或多个nonstatic data members在member function中被直接存取时,才需要class object。class object提供了this指针给这种形式的函数调用使用。这个this指针把“在member function中存取的nonstatic class members”绑定于“object内对应的members”之上。如果没有任何一个members被直接存取,事实上就不需要this指针,因此也就没有必要通过一个class object来调用一个member function。不过,C++ 语言到当前为止并不能识别这种情况。

static member functions的主要特性是它没有this指针。以下的次要特性统统根源于其主要特性:

  • 它不能够直接存取其class中的nonstatic members
  • 它不能够被声明为const、volatile或virtual
  • 它不需要经由class object才被调用,虽然大部分时候它是这样被调用的。

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

Virtual Member Functions(虚拟成员函数)

virtual function的一般实现模型:每一个class有一个virtual table,内含该class之中有作用的virtual function的地址,然后每个object有一个vptr,指向virtual table的所在。

为了支持virtual function机制,必须首先能够对于多态对象有某种形式的“执行期类型判断法(runtime type resolution)”。也就是说,以下的调用操作将需要ptr在执行期的某些相关信息,

ptr->z();

如此一来才能够找到并调用z()的适当实体。

在C++中,多态(polymorphism)表示“以一个public base class的指针(或reference),寻址出一个derived class object”的意思。例如下面的声明:

Point *ptr;

我们可以指定ptr以寻址出一个Point2d对象:

ptr = new Point2d;

或是一个Point3d对象

ptr = new Point3d;

ptr的多态机能主要扮演一个输送机制(transport mechanism)的角色,经由它,我们可以在程序的任何地方采用一组public derived类型,这种多态形式被称为消极的(passive),可以在编译期完成——virtual base class的情况除外。

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

//积极多态的常见例子
ptr->z();

在runtime type identification(RTTI)性质于1993年被引入C++ 语言之前,C++ 对“积极多态”的唯一支持,就是对virtual function call的决议(resolution)操作。有了RTTI,就能够在执行期查询一个多态的指针或多态的reference了

//积极多态的第二个例子
if(Point3d *p3d = dynamic_cast<Point3d*>(ptr))
return p3d->_z;

在实现上,可以在每一个多态对象的class object身上增加两个members:

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

表格中的virtual functions地址如何被构建起来?在C++ 中,virtual function(可经由其class object被调用)可以在编译时期获知。此外,这一组地址是固定不变的。执行期不可能新增或替换之。由于程序执行时,表格的大小和内容都不会改变,所以其建构和存取皆可以由编译器完全掌控,不需要执行期的任何介入。

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

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

每一个virtual function都被指派一个固定的索引值,这个索引在整个继承体系中保持与特定的virtual function的关系。如下的Point class体系中:

class Point{
public:
virtual ~Point(); virtual Point& mult(float) = 0; float x() const{ return _x; }
virtual float y() const { return 0; }
virtual float z() const { return 0; }
//...
protected:
Point(float x = 0.0);
float _x;
};

vitual destructo被赋值slot 1。而mult()被赋值slot 2。mult()并没有函数定义(因为它是一个pure virtual function),所以pure_virtual_calssed()的函数地址会被放在slot 2中,如果该函数意外地被调用,通常的操作是结束掉这个程序。y()被赋值slot 3而z()被赋值slot 4。下图为Point的内存布局和virtual table。

单一继承下的Virtual Functions

当一个class派生自Point时,会发生什么事情?

class Point2d : public  Point{
public:
Point2d(float x = 0.0, float y = 0.0)
: Point(x), _y(y) { }
~Point2d(); //改写base class virtual functions
Point2d &mult(float);
float y() const { return _y ;}
//... 其他操作
protected:
float _y;
};

一共有三种可能性:

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

类似的情况如下,Point3d派生自Point2d:

class Point3d : public Point2d{
public:
Point3d(float x = 0.0, float y = 0.0, float z = 0.0)
: Point2d(x, y), _z(z){ }
~Point3d(); //改写base class virtual functions
Point3d &mult(float);
float z() const { return _z; }
//...
protected:
float _z;
};

其virtual table中的slot 1 放置Point3d destructor,slot 2放置Point3d::mult()。slot 3放置继承自Point2d的y()函数地址,slot 4放置自己的z() 函数地址。Point2d,Point3d的对象布局和virtual table如下:

在一个单一继承体系中,virtual function机制的行为十分良好,不但有效率而且很容易塑造出模型来。但是在多重继承和虚拟继承中,对virtual function的支持就没有那么美好了。

多重继承下的Virtual Functions

在多重继承中支持virtual functions,其复杂读围绕在第二个及后继的base classes身上,以及“必须在执行期调整this指针”这一点。以下面的class体系为例:

class Base1{
public:
Base1();
virtual Base1();
virtual void speakClearly();
virtual Base1 *clone() const;
protected:
float data_Base1;
}; class Base2{
public:
Base2();
virtual ~Base2();
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;
};

Derived 支持virtual functions的困难度,统统落在Base2 subobject身上。有三个问题需要解决,以此而言分别是(1)virtual destructor,(2)被继承下来的Base2::mumble(),(3)一组clone()函数实体。

首先,把一个从heap中配置而得的Derived对象的地址,指定给一个Base2指针:

Base2 *pbase2 = new Derived;

新的Derived对象的地址必须调整,以指向其Base2 subobject。编译时期会产生以下的码:

//转移以支持第二个base class
Derived *tmp = new Derived;
Base2 *pbase2 = tmp ? tmp + sizeof(Base1) : 0;

如果没有这样的调整,指针的任何“非多态运用”都将失败:

//即使pbase2被指定一个Derived对象,这也应该没有问题
pbase2->data_Base2;

当程序员要删除pbase2所指的对象时:

//必须首先调用正确的virtual destructor函数实体
//然后施行delete运算符
//pbase2可能需要调整,以指出完整对象的起始点
delete pbase2;

指针必须被再一次调整,以求再一次指向Derived对象的起始处(推测它还指向Derived对象)。然而上述的offset加法却不能够在编译时期直接设定,因为pbase2所指的真正对象只有在执行期才能确定。

一般规则是,经由指向“第二或后继之base class”的指针(或reference)来调用derived class virtual function。如:

Base2 *pbase2 = new Derived;
...
delete pbase2; //invoke derived class's destructor(virtual)

该调用操作所连带的“必要的this指针调整”操作,必须在执行期完成。也就是说,offset的大小,以及把offset加到this指针上头的那一小段程序代码,必须经由编译器在某个地方插入。

比较有效率的解决办法是利用所谓的thunk。所谓thunk是以小段assembly代码,用来(1)以适当的offset值调整this指针,(2)调到virtual function去。例如,经由一个Base2指针用Derived destructor,其相关的thunk可能看起来是这个样子的:

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

Thunk技术允许virtual table slot继续内含一个简单的指针,因此多重继承不需要任何空间上的额外负担。Slots中的地址可以直接指向virtual function,也可以指向一个相关的thunk(如果需要调整this指针的话)。于是,对于那些不需要调整this指针的virtual function而言,也就不需承载效率上的额外负担。

调整this指针的第二个额外负担就是,由于两个不同的可能:(1)经由derived class(或第一个base class)调用,(2)经由第二个(或其后继)base class调用,同一函数在virtual table中可能需要多笔对应的slots。如:

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

虽然两个delete操作导致相同的Derived destructor,但它们需要两个不同的virtual table slots:

  1. pbase1不需要调整this指针(因为Base1是最左端base class之故,它已经指向Derived 对象的起始处),其virtual table slot需放置真正的destructor地址。
  2. pbase2需要调整this指针,其virtual table slot需要相关的thunk地址

在多重继承之下,一个derived class内含n-1个额外的virtual tables,n表示其上一层base classes的个数(因此,单一继承将不会有额外的virtual tables)。

针对每一个virtual tables,Derived对象中有对应的vptr。下图说明了这点,vptrs将在constructor(s)中被设定初值(经由编译器所产生的码)

用以支持“一个class拥有多个virtual tables”的传统方法是,将每一个tables以外部对象的形式产生出来,并给予独一无二的名称。例如,Derived所关联的两个tables可能有这样的名称:

vtbl_Derived;   //主要表格
vtbl_Base2_Derived; //次要表格

于是当你将一个Derived对象地址指定给一个Base1指针或Derived指针时,被处理的virtual table是主要表格vtbl_Derived。而当你将一个Derived对象地址指定给一个Base2指针时,被处理的virtual table是次要表格vtbl_Base2_Derived。

由于执行期链接器(runtime linkers)的降临(可以支持动态共享函数库),符号名称的链接变得非常缓慢。为了调节执行期链接器的效率,Sun编译器将多个virtual tables连锁为一个;指向次要表格的指针,可由主要表格名称加上一个offset获得。在这样的策略下,每一个class只有一个具名的virtual table。

有以下三种情况,第二或后继的base class会影响对virtual functions的支持。

  1. 第一种情况是,通过一个"指向第二个base class"的指针,调用derived class virtual function。例如
Base2 *ptr = new Derived;

//调用Derived::~Derived
//ptr必须向后调整sizeof(Base1)个bytes
delete ptr;

从上面那个图可以看到这个调用操作的重点:ptr指向Derived对象中的Base2 subobject;为了能够正确执行,ptr必须调整指向Derived对象的起始处。

  1. 第二种情况是第一种情况的变化,通过一个“指向derived class”的指针,调用第二个base class中一个继承而来的virtual function。在此情况下,derived class指针必须再次调整,以指向第二个base subobject。例如:
Derived *pder = new Derived;

//调用Base2::mumble()
//pder必须被向前调整sizeof(Base1)个bytes
pder->mumble();
  1. 第三种情况发生于一个语言扩充性质之下:允许一个virtual function的返回值类型有所变化,可能是base type,也可能是publicly derived type。这一点可以通过Derived::clone()函数实体来说明。clone()的Derived版本传回一个Derived class指针,默默地改写了它的两个base class的函数实体。当我们通过“指向第二个base class”的指针来调用clone()时,this指针的offset问题于是诞生:
Base2 *pb1 = new Derived;

//调用Derived * Derived::clone()
//返回值必须被调整,指向Base2 subobject
Base2 *pb2 = pb1->clone();

当进行pb1->clone()时,pb1会被调整指向Derived对象的起始地址,于是clone()的Derived版会被调用;它会传回一个指针,指向一个新的Derived对象,该对象的地址在被指定给pb2之前,必须先经过调整,以指向Base2 subobject。

虚拟继承下的Virtual Functions

考虑下面的virtual base class派生体系,从Point2d派生出Point3d:

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

其内存布局如下图:

当一个virtual base class从另一个virtual base class派生而来,并且两者都支持virtual functions和nonstatic data members时,编译器对于virtual base class的支持简直就像进了迷宫一样。建议不要在一个virtual base class中声明nonstatic data members。

指向Member Function指针(Pointer-to-Member Functions)

取一个nonstatic data member的地址,得到的结果是该member在class布局中bytes位置(再加1)。可以想象,它是一个不完整的值,它需要被绑定于某个class object的地址上,才能够被存取

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

一个指向member fucntion的指针,其声明语法如下:

double           //return type
( Point::* //class the function is member
pmf) //name of pointer to member
(); //argument list

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

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

也可以这样指定其值:

coord = &Point::y;

想要调用它,可以这样做:

(origin.*coord)();

(ptr->*corrd)();

这些操作会被编译器转化为:

(coord)(&origin);

(coord)(ptr);

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

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

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

注意下面的程序片段:

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

pmf,一个指向member function的指针,被设值为Point:

深入探索C++对象模型(四)的更多相关文章

  1. 《深度探索C++对象模型》读书笔记(一)

    前言 今年中下旬就要找工作了,我计划从现在就开始准备一些面试中会问到的基础知识,包括C++.操作系统.计算机网络.算法和数据结构等.C++就先从这本<深度探索C++对象模型>开始.不同于& ...

  2. C++的黑科技(深入探索C++对象模型)

    周二面了腾讯,之前只投了TST内推,貌似就是TST面试了 其中有一个问题,“如何产生一个不能被继承的类”,这道题我反反复复只想到,将父类的构造函数私有,让子类不能调用,最后归结出一个单例模式,但面试官 ...

  3. 读书笔记《深度探索c++对象模型》 概述

    <深度探索c++对象模型>这本书是我工作一段时间后想更深入了解C++的底层实现知识,如内存布局.模型.内存大小.继承.虚函数表等而阅读的:此外在很多面试或者工作中,对底层的知识的足够了解也 ...

  4. 柔性数组-读《深度探索C++对象模型》有感 (转载)

    最近在看<深度探索C++对象模型>,对于Struct的用法中,发现有一些地方值得我们借鉴的地方,特此和大家分享一下,此间内容包含了网上搜集的一些资料,同时感谢提供这些信息的作者. 原文如下 ...

  5. 柔性数组-读《深度探索C++对象模型》有感

    最近在看<深度探索C++对象模型>,对于Struct的用法中,发现有一些地方值得我们借鉴的地方,特此和大家分享一下,此间内容包含了网上搜集的一些资料,同时感谢提供这些信息的作者. 原文如下 ...

  6. [读书系列] 深度探索C++对象模型 初读

    2012年底-2014年初这段时间主要用C++做手游开发,时隔3年,重新拿起<深度探索C++对象模型>这本书,感觉生疏了很多,如果按前阵子的生疏度来说,现在不借助Visual Studio ...

  7. 拾遗与填坑《深度探索C++对象模型》3.3节

    <深度探索C++对象模型>是一本好书,该书作者也是<C++ Primer>的作者,一位绝对的C++大师.诚然该书中也有多多少少的错误一直为人所诟病,但这仍然不妨碍称其为一本好书 ...

  8. 拾遗与填坑《深度探索C++对象模型》3.2节

    <深度探索C++对象模型>是一本好书,该书作者也是<C++ Primer>的作者,一位绝对的C++大师.诚然该书中也有多多少少的错误一直为人所诟病,但这仍然不妨碍称其为一本好书 ...

  9. 深度探索C++对象模型

    深度探索C++对象模型 什么是C++对象模型: 语言中直接支持面向对象程序设计的部分. 对于各个支持的底层实现机制. 抽象性与实际性之间找出平衡点, 需要知识, 经验以及许多思考. 导读 这本书是C+ ...

随机推荐

  1. stm32中断学习总结

    经过了两天,终于差不多能看懂32的中断了,由于是用的库函数操作的,所以有些内部知识并没有求甚解,只是理解知道是这样的.但对于要做简单开发的我来说这些已经够了. 我学习喜欢从一个例程来看,下面的程序是我 ...

  2. DOM 以及JS中的事件

    [DOM树节点] DOM节点分为三大节点:元素节点,文本节点,属性节点. 文本节点,属性节点为元素节点的两个子节点通过getElment系列方法,可以去到元素节点 [查看节点] 1 document. ...

  3. Angularjs快速入门(四)-css类和样式

    例子: .error{background-color:red;} .warning{background-color:yellow;} <div ng-controller='HeaderCo ...

  4. 【代码学习】GD库中添加图片水印

    函数 getimagesize() bool imagecopymerge( resource dst_im, resource src_im, int dst_x, int dst_y, int s ...

  5. poj2104 Kth-Number

    Description You are working for Macrohard company in data structures department. After failing your ...

  6. mui开发app之webview是什么

    WebView(网络视图)能加载显示网页,可以将其视为一个浏览器,webview被封装在html5+,plus对象中,底层由java,OC实现. 先来谈谈我对webview的理解: 使用mui开发的a ...

  7. 在Ueditor / Umeditor中实现上传图片跨域

    近几天公司的后台管理需要图文编辑文章,但是ueditor提供的方法中,本地图片的上传是通过flash的方式处理的,且不支持跨域.若要在已经前后端分离的Angular项目中使用,需要做复杂的环境配置.跟 ...

  8. Transform java future into completable future 【将 future 转成 completable future】

    Future is introduced in JDK 1.5 by Doug Lea to represent "the result of an asynchronous computa ...

  9. 关于echarts的那些事(地图标点,折线图,饼图)

    前记:离上一篇博客的发布已经过去两个月了,这期间总想写点什么,却怎么都写不出来,一直拖到了现在.现在的感觉,不是像这期间一样,想好好整理一番,写一篇好博客,却写不出来.事实发现,随心就好,较好的博客, ...

  10. 思考题:用Use Case获取需求的方法是否有什么缺陷,还有什么地方需要改进?(提示:是否对所有的应用领域都适用?使用的方便性?.......)

    思考题: 用Use Case获取需求的方法是否有什么缺陷,还有什么地方需要改进?(提示:是否对所有的应用领域都适用?使用的方便性?.......) 简答: 一.用例解释: 在软件工程中,用例是一种在开 ...