第四章 Function语意学 (The Semantics of Function)

如果有一个Point3d的指针和对象:

Point3d obj;
Point3d *ptr = &obj;

当这样做:

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

时,会发生什么事情呢?其中的Point3d::normalize()定义如下:

Point3d Point3d::normalize() const {register float mag = magnitude();Point3d normal;normal._x = _x / mag;normal._y = _y / mag;normal._z = _z / mag;return normal;
}

而其中的Point3d::magnitude()定义如下:

float Point3d::magnitude() const {return sqrt(_x * _x + _y * _y + _z * _z);
}

答案是:不知道.
    C++支持三种类型的member functions:static,nonstatic,virtual,每一种类型被调用的方式都不相同.其间差异正是下一节的主题.不过,虽然不能够确定normalize()和magnitude()两函数是否为 virtual 或 nonvirtual,但可以确定它 一定不是 static,原因有二:(1)它直接存取nonstatic数据;(2)它被声明为 const,static member functions不可能做到这两点.

4.1 Member的各种调用方式

回顾历史,原始的"C with Classes"只支持nonstatic member functions.Virtual 函数是在20世纪80年代中期被加进来的,并且受到许多质疑.Static member functions是最后被引入的一种函数类型,它们在1987年被正式加入C++中.

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

C++的设计准则之一就是:nonstatic member functions至少必须和一般的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 指针:

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

如果member function是const,则变成:

// const nonstatic member的扩张过程
Point3d Point3d::magnitude(const Point3d *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);

本章一开始所提及的normalize()函数会被转化为下面的形式,其中假设已经声明有一个Point3d copy constructor,而named returned value(NRV)的优化也已施行:

// 以下描述"named return value函数"的内部转化
// 使用C++伪代码
void normalize_7Point3dFV(register const Point3d *const this, Point3d &__result) {register float mag = this->magnitude();// default constructor__result.Point3d::Point3d();__result._x = this->x / mag;__result._y = this->y / mag;__result._z = this->z / mag;return ;
}

一个比较有效率的做法是直接建构"normal"值,像这样:

Point3d Point3d::normalize() const {register float mag = magnitude();return Point3d(_x / mag, _y / mag, _z / mag);
}

它会被转化为以下的代码:

// 以下描述内部转化
// 使用C++伪码
void normalize_7Point3dFV(register const Point3d *const this, Point3d &__result) {register float mag = this->magnitude();// __result用以取代返回值(return value)__result.Point3d::Point3d(this->_x / mag, this->_y / mag, this->_z / mag);return ;
}

这可以 节省default constructor初始化所引起的额外负担.

名称的特殊处理 (Name Mangling)

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

class Bar {
public:int ival;
};

其中ival有可能变成这样:

// member经过name-mangling之后的可能结果之一
ival_3Bar为什么编译器要这样做?请考虑这样的派生操作(derivation):
class Foo : public Bar {
public:int iva;
};

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

// C++伪码
// Foo的内部描述
class Foo {
public:int ival_3Bar;int ival_3Foo;
};

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

class Point {
public:void x(float newX);float x();
};

转换为:

class Point {
public:void x_5Point(float newX);float x_5Point();
};

会导致两个被重载化(overloaded)的函数实体拥有相同的名称,为了让它们独一无二,唯有再加上它们的参数链表(可以从函数原型中参考得到).如果把参数类型也编码进去,就一定可以制造逐独一无二的结果,使两个x()函数有良好的转换:

class Point {
public:void x_5PointFf(float newX);float x_5PointFv();
}

以上所示的只是cfront采用的编码方法,必须承认,目前的编译器并没有统一的编码方法.
    把参数和函数名称编码在一起,编译器于是在不同的被编译模块之间达成了一种有限形式的类型检验,举个例子,如果有一个print函数被这样定义:

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

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

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

两个实体如果拥有独一无二的name mangling,那么任何不正确的调用操作在链接时期就因无法决议(resolved)而失败.有时候可以乐观地称此为"确保类型安全的链接行为"(type-safe linkage)."乐观地"是因为它只可以捕捉函数的标记(signature,即函数名称+参数数目+参数类型)错误;如果"返回类型"声明错误,就没有办法检查出来.
    当前的编译系统中,有一种所谓的demangling工具,用来拦截名称并将其转换回去.

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 table slot的索引值,关联到normalize()函数
    第二个ptr表示 this 指针.
    类似的道理,如果magnitude()也是一个 virtual function,它在normalize()中的调用操作被转换如下:

// register float mag = magnitude();
register float mag = (*this->vptr[2])(this);

此时,由于Point3d::magnitude()是在Point3d::normalize()中被调用,而后者已经由虚拟机机制而决议(resolved)妥当, 所以明确地调用"Point3d实体"会比较有效率,并因此压制由于虚拟机制而产生的不必要的重复调用操作:

// 明确的调用操作(explicity invocation)会压制虚拟机制
register float mag = Point3d::magnitude();

如果magnitude()声明为 inline 函数会更有效率,使用 class scope operator明确调用一个 virtual function,其决议(resolved)方式会和nonstatic member function一样:

register float mag = magnitude_7Point3dFv(this);

对于以下调用:

// Point3d obj;
obj.normalize();

如果编译器把它转换为:

(* obj.vptr[1])(&obj);

虽然语意正确,却没有必要.请回忆那些并不支持多态(polymorphism)的对象(1.3节),所以上述经由obj调用的函数实体只可以是Point3d::normalize()."经由一个class object调用一个virtual function".这种操作应该总是被编译器像对待一般的nonstatic member function一样加以决议(resolved):

normalize_7PointdFv(&obj);

这样优化工程的另一利益是,virtual function的一个 inline 函数实体可以被扩张(expanded)开了,因而提供极大的效益利益.

Static Member Functions (静态成员函数)

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

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

将被转换为一般的nonmember函数调用,像这样:

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

在C++引入 static member functions之前,很少见到下面这种怪异的写法:

((Point3d *)0)->object_count();

其中的object_count()只是简单传回_object_count这个 static data member.
     独立于 class object之外的存取操作,在某个时候特别重要:当 class 设计者希望支持"没有class object存在"的情况(就像前面的object_count()那样)时,程序方法上的解决之道是很奇特地把0强制转型为一个 class 指针,因而提供出一个 this 指针实体:

// 函数调用的内部转换
object_count((Point3d *)0);

至于语言层面上的解决之道,是由cfront2.0所引入的 static member functions.static member functions的主要特性就是它没有 this 指针,以下的次要特性根源于其主要特性:
    它不能够直接存取其 class 中的nonstatic members
    它不能够被声明为 const,volatile,virtual
    它不需要经由 class object才被调用(虽然大部分时候它是这样被调用的)

    "member selection"语法的使用是一种符号上的便利,它会被转化为一个直接调用操作:

if (Point3d::object_count() > 1)

如果 class object是因为某个表达式而获得的,会如何呢?例如:

if (foo().object_count() > 1)

这个表达式仍然需要被评估求值:

// 转化,以保存副作用
(void) foo();
if (Point3d::object_count() > 1)
一个 static member function,当然会被提出于 class 声明外,并给予一个经过"mangled"的适当名称,例如:
unsigned int Point3d::object_count() {
return _object_count;
}

会被cfront转化为:

// 在cfront之下的内部转化结果
unsigned int object_count_5Point3dSFv() {
return _object_count_5Point3d;
}

其中SFv表示它是个 static member function,拥有一个空白(void)的参数链表(argument list).
      如果取一个 static member function的地址,获得的将是其在内存中的位置,也就是其地址.由于 static member function没有 this 指针,所以其地址的类型并不是一个"指向class member function的指针",而是一个"nonmember函数指针",也就是说:

&Point3d::object_count();

会得到一个数值,类型是:

unsigned int(*)();

而不是:

unsigned int (Point3d::*)();

static member function由于缺乏 this 指针,因此差不多等同于nonmember function,它提供了一个意想不到的好处:成为一个callback函数,使得将C++和C-based X Window系统结合,它们也可以成功地应用在线程(threads)函数上.

C++对象模型——Member的各种调用方式(第四章)相关推荐

  1. 4.1 Member 的各种调用方式

    C++支持三种类型的member functions:static.nonstatic和virtual,每一种被调用的方式都是不同的.确定一个类成员函数不是为static:1.它能直接存取nonsta ...

  2. 利用计算机信息资源管理方式,第四章信息资源管理

    2.数据的计算(用电子表格EXCEL处理) ①利用公式计算:excel提供的一些函数,如求和.求平均值等. ②自动计算:选定单元格--右键点击状态栏,在出现的菜单中快速查看某些数据的统计值,如均值.最 ...

  3. 计算机检索技术与技巧的检索式为,第四章计算机检索技术和数据库检索方式.ppt...

    第四章计算机检索技术和数据库检索方式 第四章 计算机信息检索的基本技术与方法 一.计算机信息检索的基本技术: 布尔逻辑.截词检索.加权检索,位置算符等. 在进行计算机检索时,有时有一些比较复杂的课题, ...

  4. Cpp 对象模型探索 / 类静态成员函数的调用方式

    一.普通静态成员函数的调用方法 栗子: class CA { public:static void func() {} };int main() {CA A;A.func();CA::func();r ...

  5. Cpp 对象模型探索 / 虚函数的调用方式

    虚函数有两种调用方式: 方案1,直接使用函数地址调用. 方案2,通过对象的虚函数表指针找到虚函数表,从而得到函数地址,完成调用. 应用场景主要有如下三种情况: 当对象直接调用时,采用方案 1 调用虚函 ...

  6. 组件对象模型 COM 的 Python 调用

    关于COM的基本概念,可参考组件对象模型 COM 的内容,下面主要介绍两种使用 Python 调用 COM 组件的方法. 1 使用 win32com 1.1 环境搭建 首先需要搭建Python的环境, ...

  7. 2021年大数据常用语言Scala(十二):基础语法学习 方法调用方式

    目录 方法调用方式 后缀调用法 中缀调用法 操作符即方法 花括号调用法 无括号调用法 方法调用方式 在scala中,有以下几种方法调用方式, 后缀调用法 中缀调用法 花括号调用法 无括号调用法 在后续 ...

  8. Redis的Java客户端Jedis的八种调用方式(事务、管道、分布式…)介绍(转)

    [-] 一普通同步方式 二事务方式Transactions 三管道Pipelining 四管道中调用事务 五分布式直连同步调用 六分布式直连异步调用 七分布式连接池同步调用 八分布式连接池异步调用 九 ...

  9. Winform开发框架的业务对象统一调用方式

    在这个纷繁的社会里面,统一性的特点能够带来很多高效的产出.牢固的记忆,这种特征无论对于企业.个人的开发工作,知识的传承都有着非常重要的作用,Winfrom框架本身就是基于这个理念而生,从统一的数据库设 ...

最新文章

  1. Django快速开发之投票系统
  2. 终于有人手把手用Java实现超市管理系统
  3. CLR via C#深解笔记二 - 类型设计
  4. Android之GridLayoutManager.setSpanSizeLookup问题
  5. 支付宝五福java_支付宝五福奖金翻倍!有人拿了5328元!今天最高能翻18倍,你拿到多少?...
  6. Docker拉取镜像报错error pulling image configuration
  7. 苹果cms10的一次尝试发现了苹果cms10被挂马极有可能是苹果cms作者故意的js漏洞或后门导致
  8. 第54届超算TOP500排名,中美各自以数量和性能领先
  9. H3C设备运行状态查询常用命令(建议收藏)
  10. 弘辽科技:聚划算收费实施细则介绍
  11. 半导体无尘车间测试尘埃粒子浓度等级设备
  12. Python基础——time模块(制作秒表、倒计时)
  13. 洛谷 P3258 松鼠的新家 题解
  14. 使用mybatis plus时传入中文时出现乱码
  15. tar命令(linux解压缩命令)
  16. 水面模拟--波动方程
  17. postman查看response_Postman教程——响应
  18. CS中如何去处鼠标加速度及鼠标的相关设置
  19. 云顶之弈国际服服务器维护,【云顶之弈】国服和国际服有大量差异,国际服修复多个漏洞...
  20. 自制F1C200S demo板(四、TF卡启动)

热门文章

  1. SMT 常用术语解释
  2. 简单网页设计静态成品分享
  3. Testlink解决大用例导入问题
  4. matlab报错:位置 2 处的索引超出数组边界(不能超出 3)。
  5. Vue:如何保持导航栏的高亮状态
  6. 五霸七雄 细数ERP春秋(转)
  7. 既生 var 何生 let
  8. 利物浦大学计算机qs排名,利物浦大学qs世界排名
  9. Java 的设计模式
  10. 【DNA计算】DNA编码----笔记1