深入探索C++对象模型(四)
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的形式。 转化步骤如下:
- 改写函数的signature(译注:意指函数原型)以安插一个额外的参数到member function中,用以提供一个存取管道,使class object得以将此函数调用。该额外参数被称为this指针。
- 将每一个“对nonstatic data member的存取操作”改为经由this指针来存取。
- 将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:
- 一个字符串或数字,表示class的类型
- 一个指针,指向表格,表格中带有程序的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;
};
一共有三种可能性:
- 它可以继承base class所声明的virtual function的函数实体。正确地说,是该函数实体得地址会被拷贝到derived class的virtual table的相对应的slot之中
- 它可以实现自己的函数实体,表示它自己的函数实体地址必须放在对应的slot中
- 它可以加入一个新的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:
- pbase1不需要调整this指针(因为Base1是最左端base class之故,它已经指向Derived 对象的起始处),其virtual table slot需放置真正的destructor地址。
- 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的支持。
- 第一种情况是,通过一个"指向第二个base class"的指针,调用derived class virtual function。例如
Base2 *ptr = new Derived;
//调用Derived::~Derived
//ptr必须向后调整sizeof(Base1)个bytes
delete ptr;
从上面那个图可以看到这个调用操作的重点:ptr指向Derived对象中的Base2 subobject;为了能够正确执行,ptr必须调整指向Derived对象的起始处。
- 第二种情况是第一种情况的变化,通过一个“指向derived class”的指针,调用第二个base class中一个继承而来的virtual function。在此情况下,derived class指针必须再次调整,以指向第二个base subobject。例如:
Derived *pder = new Derived;
//调用Base2::mumble()
//pder必须被向前调整sizeof(Base1)个bytes
pder->mumble();
- 第三种情况发生于一个语言扩充性质之下:允许一个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++对象模型(四)的更多相关文章
- 《深度探索C++对象模型》读书笔记(一)
前言 今年中下旬就要找工作了,我计划从现在就开始准备一些面试中会问到的基础知识,包括C++.操作系统.计算机网络.算法和数据结构等.C++就先从这本<深度探索C++对象模型>开始.不同于& ...
- C++的黑科技(深入探索C++对象模型)
周二面了腾讯,之前只投了TST内推,貌似就是TST面试了 其中有一个问题,“如何产生一个不能被继承的类”,这道题我反反复复只想到,将父类的构造函数私有,让子类不能调用,最后归结出一个单例模式,但面试官 ...
- 读书笔记《深度探索c++对象模型》 概述
<深度探索c++对象模型>这本书是我工作一段时间后想更深入了解C++的底层实现知识,如内存布局.模型.内存大小.继承.虚函数表等而阅读的:此外在很多面试或者工作中,对底层的知识的足够了解也 ...
- 柔性数组-读《深度探索C++对象模型》有感 (转载)
最近在看<深度探索C++对象模型>,对于Struct的用法中,发现有一些地方值得我们借鉴的地方,特此和大家分享一下,此间内容包含了网上搜集的一些资料,同时感谢提供这些信息的作者. 原文如下 ...
- 柔性数组-读《深度探索C++对象模型》有感
最近在看<深度探索C++对象模型>,对于Struct的用法中,发现有一些地方值得我们借鉴的地方,特此和大家分享一下,此间内容包含了网上搜集的一些资料,同时感谢提供这些信息的作者. 原文如下 ...
- [读书系列] 深度探索C++对象模型 初读
2012年底-2014年初这段时间主要用C++做手游开发,时隔3年,重新拿起<深度探索C++对象模型>这本书,感觉生疏了很多,如果按前阵子的生疏度来说,现在不借助Visual Studio ...
- 拾遗与填坑《深度探索C++对象模型》3.3节
<深度探索C++对象模型>是一本好书,该书作者也是<C++ Primer>的作者,一位绝对的C++大师.诚然该书中也有多多少少的错误一直为人所诟病,但这仍然不妨碍称其为一本好书 ...
- 拾遗与填坑《深度探索C++对象模型》3.2节
<深度探索C++对象模型>是一本好书,该书作者也是<C++ Primer>的作者,一位绝对的C++大师.诚然该书中也有多多少少的错误一直为人所诟病,但这仍然不妨碍称其为一本好书 ...
- 深度探索C++对象模型
深度探索C++对象模型 什么是C++对象模型: 语言中直接支持面向对象程序设计的部分. 对于各个支持的底层实现机制. 抽象性与实际性之间找出平衡点, 需要知识, 经验以及许多思考. 导读 这本书是C+ ...
随机推荐
- node.js系列(实例):原生node.js实现接收前台post请求提交数据
前台界面: 前台代码: <form class="form-horizontal" method="post" action="http:127 ...
- 【Tomcat源码学习】-5.请求处理
前四章节,主要对Tomcat启动过程中,容器加载.应用加载.连接器初始化进行了相关的原理和代码流程进行了学习.接下来开始进行接受网络请求后的相关处理学习. 一.整体流程 基于上一节图示进 ...
- Xamarin.Forms+Prism(3)—— 简单提示UI的使用
这次给大家介绍两个比较好用的提示插件,如成功.等待.错误提示. 准备: 1.新建一个Prism Xamarin.Forms项目: 2.右击解决方案,添加NuGet包: 1)Acr.UserDialog ...
- 关于Java中继承多接口同名方法的问题
在Java中如果一个类同时继承接口A与B,并且这两个接口中具有同名方法,会怎么样? 动手做实验: interface A{ void fun(); } interface B{ void fun(); ...
- ajax的介绍
$.ajax({ 11 url: "article.asmx/GetArticleByID", 12 type: "POST", 13 datatype: &q ...
- C#网络程序设计(3)网络传输编程之TCP编程
网络传输编程指基于各种网络协议进行编程,包括TCP编程,UDP编程,P2P编程.本节介绍TCP编程. (1)TCP简介: TCP是TCP/IP体系中最重要的传输层协议,它提供全双工和可 ...
- Apache Spark1.1.0部署与开发环境搭建
Spark是Apache公司推出的一种基于Hadoop Distributed File System(HDFS)的并行计算架构.与MapReduce不同,Spark并不局限于编写map和reduce ...
- 函数, lambda表达式
函数 函数:简单的理解,就是一次执行很多行代码 函数的返回值 函数的参数,和变量没区别 例: def hello(): print "hello world" hello() he ...
- PHP 手册
http://www.php.net/manual/zh/index.php 感谢中文翻译工作者. PHP 手册¶ by:Mehdi Achour Friedhelm Betz Antony Dovg ...
- 【算法系列学习】[kuangbin带你飞]专题十二 基础DP1 F - Piggy-Bank 【完全背包问题】
https://vjudge.net/contest/68966#problem/F http://blog.csdn.net/libin56842/article/details/9048173 # ...