第4章 Function语意学
第4章 Function语意学
有两个成员函数这么定义
Point3d
Point3d::normalize() const {
register float mag = magnitude();
Point3d normal;
normal._x = _x / mag;
normal._y = _y / mag;
normal._z = _z / mag;
return normal;
}
float
Point3d::magnitude() const {
return aqrt(_x * _x + _y * _y + _z * _z);
}
考虑下面是怎么调用函数的
Point3d obj;
Point3d *ptr = &obj;
obj.normalize();
ptr->normalize();
并能确定函数的调用方法. C++支持三种类型的member functions: static, nonstatic和virtual, 每一种类型被调用的方式都不同.
虽然不能确定normalize()和magnitude()两函数是否为virtual或nonvirtual, 但可以确定它一定不是static, 原因有二:
- 它能直接存取nonstatic数据
- 它被声明为const(static member function没有this指针)
4.1 Member的各种调用方式
Nonstatic Member Function(非静态成员函数)
C++的设计准则之一就是: nonstatic member function至少必须和一般的nonmember function有相同的效率, 就是说编译器内部将"member函数实例"转换为对等的"nonmember函数实例"
member function转换成nonmember function步骤:
(1) 改写函数的signature(函数原型), 以安插一个额外的参数到member function中, 用以提供一个存取管道, 使class object得以将此函数调用.
Point3d Point3d::magnitude(Point3d *const this);
// 如果member function是const形式, 则转化为如下形式:
Point3d Point3d::magnitude(const Point *const this);
(2) 将每一个"对nonstatic data member的存取操作"改为经由this指针来存取
{
return sqrt(
this->_x * this->_x +
this->_y * this->_y +
this->_z * this->_z);
}
(3) 将member function重写成一个外部函数. 将函数名称经过"mangling"处理, 使得他在程序中称为独一无二的语汇:
extern magnitude_7Point3dFV(
register Point3d *const this);
函数转换好后对其调用操作进行转换:
obj.magnitude();
magnitude_7Point3dFV( &obj );
ptr->magnitude();
magnitude_7Point3dFV( ptr );
名称的特殊处理为了使经过"mangling"处理的的函数名唯一, 会综合class名称和参数链表, 若只考虑class名称则重载操作难以处理. 函数的标记: signature, 亦即函数名称+参数个数+参数类型
class Point {
public:
void x(float newX);
float x();
// ...
}
// 处理后的类
class Point {
public:
void x_5PointFf(float newX);
float x_5PointFv();
// ...
}
virtual Member Function(虚成员函数)
考虑下面两种操作
ptr->normalize();
obj.normalize();
通过ptr调用内部会转化为(* ptr->vptr[1])(ptr);
,
- vptr表示由编译器产生的指针, 指向virtual table. 它被安插进一个"声明有(或继承自)一个或多个virtual functions"的class object中. 事实上其名称也会被"mangled", 因为在一个复杂的class派生体系中, 可能存在多个vptrs
- 1是virtual table slot的索引, 关联到normalize();
- 第二个ptr表示this指针;
类似道理, 如果magnitude()也是一个virtual function, 他在normalize()之中的调用操作将会被转换成register float mag = (*this->vptr[2](this));
, 此时, 由于Point3d::magnitude()是在Point3d::normalize()中被调用的, 而后者已经由虚拟机制而决议妥当, 这时显式调用"Point3d实例"会比较有效率, 并因此压制有虚拟机制而产生的不必要的重复调用操作
register float mag = Point3d::magitude();
使用class scop operator显示调用一个virtual function, 其决议方式会和nonstatic memberfunction一样register float mag = magnitude_7Point3dFv(this);
上面两段意思就是当虚拟机制决议好时, 内部使用虚函数时, 使用虚拟机制没有直接用作用域调用更有效率, 作用域调用会压制虚拟机制
通过obj调用, 如果进行与ptr相同操作, 虽然语意正确, 却没有必要. 请会以那些并不支持多态(polymorphism)的对象(1.3节, 非引用或指针). 所以上述有obj调用的函数实例只可以是Point3d::normalize()."经由一个class object调用一个virtual function", 这种操作应该总是被编译器想对待一般nonstatic member function一样加以决议, 即normalize_7Point3dFv( &obj );
virtual functions, 特别是它们在继承机制下的行为, 将在4.2节有比较详细的讨论
static member function(静态成员函数)
static member function会被转换成一般的nonmember function, 但是不同于普通的member function, static member function没有this指针, 因此差不多等同于nonmember function. 每一个调用操作会进行类似如下转换:
// obj.normalize();
normalize_7Point3dSFv();
// ptr->normalize();
normalize_7Point3dSFv();
假设Point3d类存在一个名为object_count的static member function:
unsigned int Point3d::object_cout() {
return _object_count;
}
// 会被cfront转化为:
unsigned int object_cout__5Point3dSFv() {
return _boject_cout__5Point3d;
}
SFv表示他是一个static member function, 拥有一个空白(void)参数链表
如果取自一个static member function的地址, 获得的将使其在内存中的位置, 也就是其地址. 由于static member function没有this指针, 所以其地址的类型并不是一个"指向class member function的指针", 而是一个"nonmember函数指针":
&Point3d::object_cout();
// 会得到一个数值
unsigned int (*)(); // 函数指针类型
// 而不是
unsigned int (Point3d::*)(); // 类成员指针
static member functions的主要特性就是它没有this指针, 因此有如下特性
(1) **它不能直接存取其class中的nonstatic members. 就是不能调用非静态成员**
(2) **它不能够被声明为const, volatile或virtual**
(3) **它不需要(不是不能)经由class object才被调用, 虽然大部分时候它是这样被调用的!**
4.2 Virtual Member Functions(虚函数)
在C++中, 多态(polymorphism)表示"以一个public base class的指针(或reference), 寻址出一个derived class object"的意思
Point *ptr;
ptr = new Point2d;
// 或是
ptr = new Point3d;
ptr的多态机能主要扮演一个输送机制(transport mechanism)的角色, 经由它, 可以在程序的任何地方采用一组public derived类型. 这种多态形式被称为是消极的(passive), 可以在编译时期完成--virtual base class的情况除外
当被指出的对象真正被使用时, 多态也就变成积极的(active), 如: ptr->z();
, 积极多态(active polymorphism)的常见例子
在runtime type identification(RTTI)性质于1993年被引入C++语言之前, C++对"积极多态(active polymorphism)"的唯一支持, 就是对于virtual function call的决议(resolution)操作. 有了RTTI, 就能在执行期查询一个多态的pointer或多态的reference了(RTTI在7.3节讨论)
ptr->z()
, 其中z()是一个virtual function, 那么什么信息才能在执行期调用正确的z()实例? 需要知道: (1) ptr所指对象的真实类型. 这可以选择正确的z()实例; (2) z()实例的位置, 以便能够调用它
- 在实现上, 首先可以在每一个多态的class object身上增加两个members:
(1)一个字符串或数字, 表示class类型
(2)一个指针, 指向某表格, 表格中持有程序的virtual functions的执行期地址
表格中的virtual functions地址如何被构建起来? 在C++中, virtual functions(可经由其class object被调用)可以在编译时期获知. 此外, 这一组地址是固定不变的, 执行期不可能新增或替换之. 由于程序执行时, 表格的大小和内容都不会改变, 所以其建构和存取皆可以有编译器完全掌控, 不需要执行期的任何接入 - 然而, 执行期备妥那些函数地址, 只是解答的一半而已. 另一半的解答是找到那些地址
(1)为了找到表格, 每一个class object被安插了一个由编译器内部产生的指针, 指向该表格
(2)为了找到函数地址, 每一个virtual function被指派一个表格索引值
这些工作都是由编译器完成. 执行期要做的, 只是在特定的virtual table slot(记录这virtual function的地址)中激活virtual function
一个class只会有一个virtual table. 每一个table内含其对应之class object中所有active virtual functions函数实例的地址. 这些active virtual function包括:
(1)这一class所定义的函数实例. 它会改写(overriding)一个可能存在的base class virtual function函数实例
(2)继承自base class的函数实例. 这是在derived class决定不改写virtual function时才会出现的情况
(3)一个pure_virtual_called()函数实例, 它既可以扮演pure virtual function的空间保卫者角色, 也可以当做执行期异常处理函数(有时会用到)----> 没理解什么意思
一个derived class新增一个base class没有的virtual function这个derived class的大小没变, 还是用的原来的virtual function table, 只是把这个函数插进去. 不过新增virtual function没太大意义, 因为base 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;
};
class Point2d : public Point {
public:
Point2d( float x = 0.0, float y = 0.0)
: Point(x), _y(y) {}
~Point2d();
// 改写base class virtual function
Point2d &mult( float );
float y() const { return _y; }
protected:
float _y;
};
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 function
Point3d &mult( float );
float z() const { return _z; }
protected:
float _z;
};
virtual table布局:
对ptr->z();
调用, 如何有足够知识在编译期设定virtual function调用?
- 一般而言, 在每次调用z()时, 并不知道ptr所指对象的真正类型. 然而却知道, 经由ptr可以存取到该对象的virtual table
- 虽然不知道哪一个z()函数会被调用, 但知道每一个z()函数的地址都被放在slot4中
这些信息使得编译器可以将该调用转化为( *ptr->vptr[ 4 ] )(ptr);
唯一一个在执行期才能知道的东西是: slot4所指的到底是哪一个z()函数实例
单一继承体系中, virtual function机制的行为十分良好, 不但有效率而且容易塑造出模型来. 但是在多继承和虚继承之中, 对virtual function的指针就没那么美好了
多重继承下的Virtual Function
在多重继承中支持virtual function, 其复杂度围绕在第二个及后继的base classes身上, 以及"必须在执行期调整this指针"这一点
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 numble();
virtual Base2 *clone() cosnt;
protected:
float data_Base2;
};
class Derived : public Base1, public Base2 {
public:
Derived();
virtual ~Derived();
virtual Derived *clone() const;
protected:
float data_Derived;
};
以此里而言分别是: (1) virtual destructor; (2) 被继承下来的Base2::numble(); (3) 一组clone()函数实例
在多重继承之下, 一个derived class内含n-1个额外的virtual tables, n表示上一层classes的个数(因此, 单一继承将不会有额外的virtual tables). 对于上面derived而言, 会有两个virtual tables被编译出来:
(1) 一个主要实例, 与Base1(最左端base class)共享
(2) 一个次要实例, 与Base2(第二个base class)有关
针对每一个virtual table, derived对象中有对应的vptr
用以支持"一个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
有3中情况, 第二或后继的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对象的起始处
(2) 通过一个指向"derived class"的指针, 调整第二个base class中一个继承而来的virtual function.
Derived *pder = new Derived;
// 调用Base2::numble()
// pder必须被向前调整sizeof(Base1)个bytes
pder->numble();
在这种情况下, derived class指针必须再次调整, 以指向第二个base subobject
(3) 允许一个virtual function的返回值类型有所变化, 可能是base type, 也可能是publicly derived type, 这一点可以经由Derived::clone()函数实例来说明
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
class Point2d {
public:
Point2d( float = 0.0, float = 0.0 );
virtual ~Point2d();
virtual void numble();
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 table布局如下
虽然Point3d有唯一一个base class, 及Point2d, 但Point3d和Point2d的起始部分并不像"非虚拟的单一继承"情况那样一致. 如果上图所示, 由于Point2d和Point3d的对象不在相符, 两者之间的转换也就需要调整this指针. 至于在虚继承的情况下要消除thunks, 一般而言已被证明是一种高难度技术
当一个virtual base class从另一个virtual base class派生而来, 并且两者都支持virtual functions和nonstatic data members时, 编译器对于virtual base class的支持像是尽量迷宫一样.
作则建议: 不要再一个virtual base class中声明nonstatic data members
4.3 函数的效能
略
4.4 指向Member Function的指针
class Point3d {
public:
Point3d(float x = 0.0, float y = 0.0, float z = 0.0) : _x(x), _y(y), _z(z) {}
float x() { return _x; }
float y() { return _y; }
float z() { return _z; }
private:
float _x, _y, _z;
};
int main() {
Point3d p;
Point3d *pp = &p;
// 定义并初始化Member Function指针
float (Point3d::*pmf1) () = &Point3d::x;
// 调用
(p.*pmf1)(); // 会被编译器转换为: (pmf1)(&p);
(pp->*pmf1)(); // 会被编译器转换为: (pmf1)(pp);
// 显示地址
printf("&Point3d::x = %p\n", &Point3d::x);
printf("&Point3d::y = %p\n", &Point3d::y);
printf("&Point3d::z = %p\n", &Point3d::z);
return 0;
}
/*
&Point3d::x = 00F41447
&Point3d::y = 00F4144C
&Point3d::z = 00F41451
*/
取一个nonstatic member function的地址, 如果该函数是nonvirtual, 得到的结果是它在内存中的真正地址. 然而这个值是不完全的. 它需要被绑定于某个class object的地址上, 声明语法:
double // return type
( Point::* // class the function is member
pmf ) // name of pointer to member
(); // arguement list
// 定义并初始化该指针
double (Point::*coord)() = &Point::x;
// 也可以这样指定其值:
coord = &Point::y;
// 两中调用方法
(origin.*coord)();
(ptr->*coord)();
// 上面两种操作会被编译器转化为
// 虚拟C++码
(coord)(&origin);
(coord)(ptr);
取一个static member function(没有this指针)的类型是"函数指针", 而不是"指向member function的指针"
使用一个"member function指针", 如果不用于virtual function, 多继承, virtual base class等情况的话, 并不会比使用一个"nonmember function指针"的成本更高. 上述三种情况对于"member function指针"的类型以及调用都太过复杂. 对于那些没有virtual functions, virtual base class或multiple base classes的classes而言, 编译器可以为它们提供相同的效率
支持"指向 Virtual Member Functions"的指针
对一个nonstatic member function取地址, 将获得该函数在内存中的地址. 然而面对一个virtual function, 其地址在编译时期是未知的, 所能知道的仅仅是virtual function在其相关之virtual table中的索引值, 也就是说, 对一个virtual member function取其地址, 所能获得的只是一个索引值
编译器必须定义pmf, 使它能够(1)持有两种数值;(2)更重要的是其值可以被区别代表内存中地址还是virtual table中的索引值
cfront2.0非正式版中, 这两个值被内含在一个普通的指针内. 他用了以下技巧
((( int ) pmf ) & ~127)
? // non-virtual invocation
( *pmf )( ptr )
: // virtual invocation
(* ptr->vptr[ (int) pmf ]( ptr ));
这种实现技巧必须假设继承体系中最多支持128个virtual functions. 这并不是我们所希望的, 但却证明是可行的. 然而, 多重继承的引入, 导致需要更一般化的实现模式, 并趁机除去对virtual functions的个数限制
/*
vs2103有点疑问, 取虚函数地址不是索引
*/
#include <iostream>
using namespace std;
class Point3d {
public:
Point3d(float x = 0.0, float y = 0.0, float z = 0.0) : _x(x), _y(y), _z(z) {}
virtual float x() { return _x; }
virtual float y() { return _y; }
virtual float z() { return _z; }
private:
float _x, _y, _z;
};
int main() {
Point3d p;
Point3d *pp = &p;
// 定义并初始化Member Function指针
float (Point3d::*pmf1) () = &Point3d::x;
// 调用
(p.*pmf1)(); // 会被编译器转换为: (pmf1)(&p);
(pp->*pmf1)(); // 会被编译器转换为: (pmf1)(pp);
// 显示地址
printf("&Point3d::x = %p\n", &Point3d::x);
printf("&Point3d::y = %p\n", &Point3d::y);
printf("&Point3d::z = %p\n", &Point3d::z);
return 0;
}
/*
&Point3d::x = 00A1121C
&Point3d::y = 00A1130C
&Point3d::z = 00A112EE
*/
多重继承之下, 指向Member Functions的指针
为了让指向member functions的指针也能够支持多重继承和虚继承, Stroustrup设计了下面一个结构体:
// 一般结构体, 用以支持
// 在多重继承之下指向member functions的指针
struct __mptr {
int delta;
int index;
union {
ptrtofunc faddr;
int v_offset;
};
};
此模型下, 像调用(ptr->*pmf)();
, 会变成:
( pmf.index < 0 )
? // non-virtaul invocation
( *pmf.faddr )( ptr )
: // virtual invocation
( * ptr->vptr[pmf.index] (ptr) );
这种方法存在下面两个问题:
(1) 每一个调用操作都得付出上述成本, 检查其是否为virtual或nonvirtual
(2) 当传递一个不变值的指针给member function时, 需要产生一个临时对象, 例如
extern Point3d foo( const Point3d&, Point3d (Point3d::*)() );
void bar( const Point3d& p ) {
Point3d pt = foo(p, &Point::normal);
}
// 其中&Point3d::normal的值类似这样: {0, -1, 10727417}
// 将需要产生一个临时对象, 有明确初值:
// 虚拟C++码
__mptr temp = {0, -1, 10727417}
foo(p, temp);
4.5 Inline Function
略
第4章 Function语意学的更多相关文章
- 【C++对象模型】第四章 Function 语意学
1.Member的各种调用方式 1.1 Nonstatic Member Functions 实际上编译器是将member function被内化为nonmember的形式,经过下面转化步骤: 1.给 ...
- 《深度探索C++对象模型》第二章 | 构造函数语意学
默认构造函数的构建操作 默认构造函数在需要的时候被编译器合成出来.这里"在需要的时候"指的是编译器需要的时候. 带有默认构造函数的成员对象 如果一个类没有任何构造函数,但是它包含一 ...
- 第十一章 Function类型
在ECMAScript中,Function(函数)类型实际上是对象.每个函数也是Function类型的实例,而且都与其它引用类型一样具有属性和方法.由于是函数对象,因此函数名实际上也是一个指向函数对象 ...
- Function 语意学
C++支持三种类型的member functions: static.nonstatic和virtual,每一种类型调用方式都不相同. 一 nostatic members functions 1 调 ...
- 《深度探索C++对象模型》笔记——Function语意学
member的各种调用方式 C++支持三种类型的member functions:static.nonstatic和virtual. nonstatic member functions会被编译器转换 ...
- 【C++对象模型】第六章 执行期语意学
执行期语意学,即在程序执行时,编译器产生额外的指令调用,确保对象的构造,内存的释放,以及类型转换与临时对象的生成的安全进行. 1.对象的构造和析构 对于类对象的构造,一般在定义之后则开始内部的构造过程 ...
- 【C++对象模型】第二章 构造函数语意学
1.Default Constructor 当编译器需要的时候,default constructor会被合成出来,只执行编译器所需要的任务(将members适当初始化). 1.1 带有 Defau ...
- 深度探索C++对象模型读书笔记-第六章执行期语意学
在函数中,编译器会帮助将析构函数(Destructor) 安插在相应的位置.对于函数中的局部对象,会将析构函数安插在对象的每一个离开点. 例如: 1: void Function(int a) { 2 ...
- 第3章 Data语意学
在C++中经常会遇到一个类的大小问题,关于一个类的大小一般受到三个方面的影响. 语言本身所造成的额外负担,如在虚拟继承中会遇到如派生类中会包含一个指针指向base class subobjec,这样会 ...
随机推荐
- 050 01 Android 零基础入门 01 Java基础语法 05 Java流程控制之循环结构 12 continue语句
050 01 Android 零基础入门 01 Java基础语法 05 Java流程控制之循环结构 12 continue语句 本文知识点:continue语句 continue语句 continue ...
- Arduino 与 SPI 结合使用 以及SPI 深层理解
本文主要讲解两部分内容,不做任何转发,仅个人学习记录: 一. Arduino 与 SPI 结合使用 : 二. SPI 深层理解 有价值的几个好的参考: 1. 中文版: https://blog.cs ...
- 多测师讲解接口自动化测试 _requests_高级讲师肖sir
rep=requests.post 错误方法: 1.在代理中---把高级中----代理-----去除勾选,调用失败
- MeteoInfoLab脚本示例:MODIS AOD
MODIS的气溶胶光学厚度(AOD)产品应用很广,数据可以在Giovanni上下载:http://disc.sci.gsfc.nasa.gov/giovanni/overview/index.html ...
- day61 Pyhton 框架Django 04
内容回顾 1.django处理请求的流程: 1. 在浏览器的地址栏输入地址,回车发get请求: 2. wsgi模块接收请求: 3. 在urls.py文件中匹配地址,找到对应的函数: 4. 执行函数,返 ...
- js、css等文件引入空白问题
路径没错,不管路径怎么改变,js.css等文件就是引入失败.很多时候是因为Spring的过滤器把js.css等资源文件拦截了.default是tomcat配置的一个servlet,"Defa ...
- scrapy LinkExtractors
class scrapy.linkextractors.LinkExtractor Link Extractors 的目的很简单: 提取链接。 每个LinkExtractor有唯一的公共方法是 ext ...
- pytest文档52-命令行参数--setup-show查看fixture的执行过程
前言 使用命令行运行 pytest 用例的时候,看不到 fixture 的执行过程. 如果我们想知道fixture的执行过程和先后顺序,可以加上 --setup-show 命令行参数,帮助查看 fix ...
- lumen-ioc容器测试 (2)
lumen-ioc容器测试 (1) lumen-ioc容器测试 (2) lumen-ioc容器测试 (3) lumen-ioc容器测试 (4) lumen-ioc容器测试 (5) lumen-ioc容 ...
- php使用xpath爬取内容
<?php $html = file_get_contents('https://tieba.baidu.com/f?kw=%C9%EE%BB%A7&fr=ala0&loc=re ...